@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.js CHANGED
@@ -1,20 +1,20 @@
1
- // src/canvas/render-node.tsx
2
- import { jsx } from "jsx-dom/jsx-runtime";
3
- function renderNode(node) {
4
- return /* @__PURE__ */ jsx("div", { children: node?.id || "" });
5
- }
6
-
7
- // src/graph/types/graph.ts
1
+ // src/graph/graph.ts
8
2
  import { Map as IMap, List as IList, Set as ISet6 } from "immutable";
9
3
 
10
- // src/graph/types/node.ts
4
+ // src/graph/node.ts
11
5
  import { Record, Set as ISet } from "immutable";
12
- var defaultNodeProps = {
6
+ var defNodeData = {
13
7
  id: "",
8
+ data: void 0,
9
+ version: 0,
10
+ title: void 0,
11
+ text: void 0,
12
+ type: void 0,
13
+ render: void 0,
14
+ ports: { in: null, out: null },
14
15
  aligned: {},
15
16
  edges: { in: ISet(), out: ISet() },
16
17
  segs: { in: ISet(), out: ISet() },
17
- ports: { in: [], out: [] },
18
18
  layerId: "",
19
19
  isDummy: false,
20
20
  isMerged: false,
@@ -25,29 +25,35 @@ var defaultNodeProps = {
25
25
  dims: void 0,
26
26
  mutable: false
27
27
  };
28
- var Node = class _Node extends Record(defaultNodeProps) {
28
+ var Node = class _Node extends Record(defNodeData) {
29
29
  static dummyPrefix = "d:";
30
- get edgeId() {
31
- if (!this.isDummy)
32
- throw new Error(`node ${this.id} is not a dummy`);
33
- if (this.isMerged)
34
- throw new Error(`node ${this.id} is merged`);
35
- return this.get("edgeIds")[0];
36
- }
37
- get edgeIds() {
38
- if (!this.isDummy)
39
- throw new Error(`node ${this.id} is not a dummy`);
40
- if (!this.isMerged)
41
- throw new Error(`node ${this.id} is not merged`);
42
- return this.get("edgeIds");
30
+ // get edgeId(): EdgeId {
31
+ // if (!this.isDummy)
32
+ // throw new Error(`node ${this.id} is not a dummy`)
33
+ // if (this.isMerged)
34
+ // throw new Error(`node ${this.id} is merged`)
35
+ // return this.get('edgeIds')[0]
36
+ // }
37
+ // get edgeIds(): EdgeId[] {
38
+ // if (!this.isDummy)
39
+ // throw new Error(`node ${this.id} is not a dummy`)
40
+ // if (!this.isMerged)
41
+ // throw new Error(`node ${this.id} is not merged`)
42
+ // return this.get('edgeIds')
43
+ // }
44
+ get key() {
45
+ return this.isDummy ? this.id : _Node.key(this);
46
+ }
47
+ static key(node) {
48
+ return `${node.id}:${node.version}`;
43
49
  }
44
50
  static isDummyId(nodeId) {
45
51
  return nodeId.startsWith(_Node.dummyPrefix);
46
52
  }
47
- static addNormal(g, props) {
53
+ static addNormal(g, data) {
48
54
  const layer = g.layerAt(0);
49
55
  const node = new _Node({
50
- ...props,
56
+ ...data,
51
57
  edges: { in: ISet(), out: ISet() },
52
58
  segs: { in: ISet(), out: ISet() },
53
59
  aligned: {},
@@ -59,10 +65,10 @@ var Node = class _Node extends Record(defaultNodeProps) {
59
65
  g.dirtyNodes.add(node.id);
60
66
  return node;
61
67
  }
62
- static addDummy(g, props) {
63
- const layer = g.getLayer(props.layerId);
68
+ static addDummy(g, data) {
69
+ const layer = g.getLayer(data.layerId);
64
70
  const node = new _Node({
65
- ...props,
71
+ ...data,
66
72
  id: `${_Node.dummyPrefix}${g.nextDummyId++}`,
67
73
  edges: { in: ISet(), out: ISet() },
68
74
  segs: { in: ISet(), out: ISet() },
@@ -78,6 +84,12 @@ var Node = class _Node extends Record(defaultNodeProps) {
78
84
  g.dirtyNodes.add(node.id);
79
85
  return node;
80
86
  }
87
+ static del(g, node) {
88
+ return g.getNode(node.id).delSelf(g);
89
+ }
90
+ static update(g, data) {
91
+ return g.getNode(data.id).mut(g).merge(data);
92
+ }
81
93
  mut(g) {
82
94
  if (this.mutable) return this;
83
95
  return g.mutateNode(this);
@@ -100,6 +112,9 @@ var Node = class _Node extends Record(defaultNodeProps) {
100
112
  isUnlinked() {
101
113
  return this.edges.in.size == 0 && this.edges.out.size == 0 && this.segs.in.size == 0 && this.segs.out.size == 0;
102
114
  }
115
+ hasPorts() {
116
+ return !!this.ports?.in?.length || !!this.ports?.out?.length;
117
+ }
103
118
  layerIndex(g) {
104
119
  return this.getLayer(g).index;
105
120
  }
@@ -108,11 +123,9 @@ var Node = class _Node extends Record(defaultNodeProps) {
108
123
  }
109
124
  setIndex(g, index) {
110
125
  if (this.index == index) return this;
111
- console.log(`set index of ${this.id} to ${index}`);
112
126
  return this.mut(g).set("index", index);
113
127
  }
114
128
  setLayerPos(g, lpos) {
115
- console.log("setLayerPos", this.id, lpos);
116
129
  if (this.lpos == lpos) return this;
117
130
  return this.mut(g).set("lpos", lpos);
118
131
  }
@@ -126,7 +139,6 @@ var Node = class _Node extends Record(defaultNodeProps) {
126
139
  return this;
127
140
  }
128
141
  moveToLayer(g, layer) {
129
- console.log("moveToLayer", this, this.getLayer(g), layer);
130
142
  this.getLayer(g).delNode(g, this.id);
131
143
  layer.addNode(g, this.id);
132
144
  return this.setLayer(g, layer.id);
@@ -248,16 +260,44 @@ var Node = class _Node extends Record(defaultNodeProps) {
248
260
  }
249
261
  };
250
262
 
251
- // src/graph/types/edge.ts
263
+ // src/graph/edge.ts
252
264
  import { Record as Record2 } from "immutable";
253
- var defaultEdgeProps = {
265
+
266
+ // src/log.ts
267
+ var levels = {
268
+ error: 0,
269
+ warn: 1,
270
+ info: 2,
271
+ debug: 3
272
+ };
273
+ var currentLevel = "debug";
274
+ function shouldLog(level) {
275
+ return levels[level] <= levels[currentLevel];
276
+ }
277
+ function logger(module) {
278
+ return {
279
+ error: (msg, ...args) => shouldLog("error") && console.error(`[${module}] ${msg}`, ...args),
280
+ warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module}] ${msg}`, ...args),
281
+ info: (msg, ...args) => shouldLog("info") && console.info(`[${module}] ${msg}`, ...args),
282
+ debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module}] ${msg}`, ...args)
283
+ };
284
+ }
285
+ var log = logger("core");
286
+
287
+ // src/graph/edge.ts
288
+ var log2 = logger("edge");
289
+ var defEdgeData = {
254
290
  id: "",
291
+ data: null,
292
+ label: void 0,
255
293
  source: { id: "" },
256
294
  target: { id: "" },
295
+ type: void 0,
296
+ style: void 0,
257
297
  mutable: false,
258
298
  segIds: []
259
299
  };
260
- var Edge = class _Edge extends Record2(defaultEdgeProps) {
300
+ var Edge = class _Edge extends Record2(defEdgeData) {
261
301
  static prefix = "e:";
262
302
  mut(g) {
263
303
  if (this.mutable) return this;
@@ -315,37 +355,44 @@ var Edge = class _Edge extends Record2(defaultEdgeProps) {
315
355
  return _Edge.str(this);
316
356
  }
317
357
  static str(edge) {
318
- let source = edge.source.id;
319
- if (edge.source.port)
358
+ let source = edge.source?.id;
359
+ if (!source) throw new Error("edge source is undefined");
360
+ if (edge.source?.port)
320
361
  source = `${source} (port ${edge.source.port})`;
321
- let target = edge.target.id;
322
- if (edge.target.port)
362
+ let target = edge.target?.id;
363
+ if (!target) throw new Error("edge target is undefined");
364
+ if (edge.target?.port)
323
365
  target = `${target} (port ${edge.target.port})`;
324
366
  let str = `edge from ${source} to ${target}`;
325
367
  if (edge.type) str += ` of type ${edge.type}`;
326
368
  return str;
327
369
  }
328
- static id(edge, prefix = _Edge.prefix, side = "both") {
370
+ static key(edge, prefix = _Edge.prefix, side = "both") {
329
371
  let source = "", target = "";
330
372
  if (side == "source" || side == "both") {
373
+ if (!edge.source?.id) throw new Error("edge source is undefined");
331
374
  source = edge.source.id;
332
- if (edge.source.port)
375
+ if (edge.source?.port)
333
376
  source = `${source}.${edge.source.port}`;
377
+ const marker = edge.source?.marker ?? edge.style?.marker?.source;
378
+ if (marker && marker != "none") source += `[${marker}]`;
334
379
  source += "-";
335
380
  }
336
381
  if (side == "target" || side == "both") {
382
+ if (!edge.target?.id) throw new Error("edge target is undefined");
337
383
  target = edge.target.id;
338
384
  if (edge.target.port)
339
385
  target = `${target}.${edge.target.port}`;
340
386
  target = "-" + target;
387
+ const marker = edge.target?.marker ?? edge.style?.marker?.target ?? "arrow";
388
+ if (marker && marker != "none") target += `[${marker}]`;
341
389
  }
342
390
  const type = edge.type || "";
343
391
  return `${prefix}${source}${type}${target}`;
344
392
  }
345
- static add(g, props) {
393
+ static add(g, data) {
346
394
  const edge = new _Edge({
347
- ...props,
348
- id: _Edge.id(props),
395
+ ...data,
349
396
  segIds: []
350
397
  });
351
398
  edge.link(g);
@@ -353,18 +400,39 @@ var Edge = class _Edge extends Record2(defaultEdgeProps) {
353
400
  g.dirtyEdges.add(edge.id);
354
401
  return edge;
355
402
  }
403
+ static del(g, data) {
404
+ return g.getEdge(data.id).delSelf(g);
405
+ }
406
+ static update(g, data) {
407
+ let edge = g.getEdge(data.id);
408
+ let relink = false;
409
+ if (data.source.id !== edge.source.id || data.target.id !== edge.target.id || data.source.port !== edge.source.port || data.target.port !== edge.target.port || data.type !== edge.type) {
410
+ for (const seg of edge.segs(g))
411
+ seg.delEdgeId(g, edge.id);
412
+ edge.unlink(g);
413
+ relink = true;
414
+ }
415
+ edge = edge.mut(g).merge(data);
416
+ if (relink)
417
+ edge.link(g);
418
+ return edge;
419
+ }
356
420
  };
357
421
 
358
- // src/graph/types/seg.ts
422
+ // src/graph/seg.ts
359
423
  import { Record as Record3, Set as ISet2 } from "immutable";
360
- var defaultSegProps = {
424
+ var defSegData = {
361
425
  id: "",
362
426
  source: { id: "" },
363
427
  target: { id: "" },
428
+ type: void 0,
429
+ style: void 0,
364
430
  edgeIds: ISet2(),
431
+ trackPos: void 0,
432
+ svg: void 0,
365
433
  mutable: false
366
434
  };
367
- var Seg = class _Seg extends Record3(defaultSegProps) {
435
+ var Seg = class _Seg extends Record3(defSegData) {
368
436
  static prefix = "s:";
369
437
  mut(g) {
370
438
  if (this.mutable) return this;
@@ -389,7 +457,7 @@ var Seg = class _Seg extends Record3(defaultSegProps) {
389
457
  sameEnd(other, side) {
390
458
  const mine = this[side];
391
459
  const yours = other[side];
392
- return mine.id === yours.id && mine.port === yours.port;
460
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
393
461
  }
394
462
  setPos(g, source, target) {
395
463
  return this.mut(g).merge({
@@ -447,10 +515,10 @@ var Seg = class _Seg extends Record3(defaultSegProps) {
447
515
  }
448
516
  return this.mut(g).set("edgeIds", this.edgeIds.asMutable().remove(edgeId));
449
517
  }
450
- static add(g, props) {
518
+ static add(g, data) {
451
519
  const seg = new _Seg({
452
- ...props,
453
- id: Edge.id(props, _Seg.prefix)
520
+ ...data,
521
+ id: Edge.key(data, _Seg.prefix)
454
522
  });
455
523
  seg.link(g);
456
524
  g.segs.set(seg.id, seg);
@@ -459,33 +527,10 @@ var Seg = class _Seg extends Record3(defaultSegProps) {
459
527
  }
460
528
  };
461
529
 
462
- // src/graph/types/layer.ts
530
+ // src/graph/layer.ts
463
531
  import { Record as Record4, Set as ISet3 } from "immutable";
464
-
465
- // src/log.ts
466
- var levels = {
467
- error: 0,
468
- warn: 1,
469
- info: 2,
470
- debug: 3
471
- };
472
- var currentLevel = "debug";
473
- function shouldLog(level) {
474
- return levels[level] <= levels[currentLevel];
475
- }
476
- function logger(module) {
477
- return {
478
- error: (msg, ...args) => shouldLog("error") && console.error(`[${module}] ${msg}`, ...args),
479
- warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module}] ${msg}`, ...args),
480
- info: (msg, ...args) => shouldLog("info") && console.info(`[${module}] ${msg}`, ...args),
481
- debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module}] ${msg}`, ...args)
482
- };
483
- }
484
- var log = logger("core");
485
-
486
- // src/graph/types/layer.ts
487
- var log2 = logger("layer");
488
- var defaultLayerProps = {
532
+ var log3 = logger("layer");
533
+ var defLayerData = {
489
534
  id: "",
490
535
  index: 0,
491
536
  nodeIds: ISet3(),
@@ -496,7 +541,7 @@ var defaultLayerProps = {
496
541
  isSorted: false,
497
542
  mutable: false
498
543
  };
499
- var Layer = class extends Record4(defaultLayerProps) {
544
+ var Layer = class extends Record4(defLayerData) {
500
545
  static prefix = "l:";
501
546
  mut(g) {
502
547
  if (this.mutable) return this;
@@ -578,7 +623,6 @@ var Layer = class extends Record4(defaultLayerProps) {
578
623
  }
579
624
  setSorted(g, nodeIds) {
580
625
  if (this.hasSortOrder(nodeIds)) return this;
581
- console.log(`setting sorted for layer ${this.id}`);
582
626
  nodeIds.forEach((nodeId, i) => g.getNode(nodeId).setIndex(g, i));
583
627
  return this.mut(g).merge({ sorted: nodeIds, isSorted: true });
584
628
  }
@@ -588,7 +632,7 @@ var Layer = class extends Record4(defaultLayerProps) {
588
632
  }
589
633
  };
590
634
 
591
- // src/graph/types/mutator.ts
635
+ // src/graph/mutator.ts
592
636
  var Mutator = class {
593
637
  changes;
594
638
  constructor() {
@@ -597,20 +641,27 @@ var Mutator = class {
597
641
  removedNodes: [],
598
642
  updatedNodes: [],
599
643
  addedEdges: [],
600
- removedEdges: []
644
+ removedEdges: [],
645
+ updatedEdges: []
601
646
  };
602
647
  }
648
+ describe(description) {
649
+ this.changes.description = description;
650
+ }
603
651
  addNode(node) {
604
652
  this.changes.addedNodes.push(node);
605
653
  }
606
654
  addNodes(...nodes) {
607
655
  nodes.forEach((node) => this.addNode(node));
608
656
  }
657
+ removeNode(node) {
658
+ this.changes.removedNodes.push(node);
659
+ }
660
+ removeNodes(...nodes) {
661
+ nodes.forEach((node) => this.removeNode(node));
662
+ }
609
663
  updateNode(node) {
610
- if (typeof node === "string")
611
- this.changes.updatedNodes.push({ id: node });
612
- else
613
- this.changes.updatedNodes.push(node);
664
+ this.changes.updatedNodes.push(node);
614
665
  }
615
666
  updateNodes(...nodes) {
616
667
  nodes.forEach((node) => this.updateNode(node));
@@ -621,21 +672,18 @@ var Mutator = class {
621
672
  addEdges(...edges) {
622
673
  edges.forEach((edge) => this.addEdge(edge));
623
674
  }
624
- removeNode(node) {
625
- if (typeof node === "string")
626
- this.changes.removedNodes.push({ id: node });
627
- else
628
- this.changes.removedNodes.push(node);
629
- }
630
- removeNodes(...nodes) {
631
- nodes.forEach((node) => this.removeNode(node));
632
- }
633
675
  removeEdge(edge) {
634
676
  this.changes.removedEdges.push(edge);
635
677
  }
636
678
  removeEdges(...edges) {
637
679
  edges.forEach((edge) => this.removeEdge(edge));
638
680
  }
681
+ updateEdge(edge) {
682
+ this.changes.updatedEdges.push(edge);
683
+ }
684
+ updateEdges(...edges) {
685
+ edges.forEach((edge) => this.updateEdge(edge));
686
+ }
639
687
  };
640
688
 
641
689
  // src/graph/services/cycles.ts
@@ -735,14 +783,12 @@ var Cycles = class _Cycles {
735
783
 
736
784
  // src/graph/services/dummy.ts
737
785
  import { Set as ISet4 } from "immutable";
738
- var log3 = logger("dummy");
786
+ var log4 = logger("dummy");
739
787
  var Dummy = class _Dummy {
740
788
  static updateDummies(g) {
741
- log3.debug(`updating dummies:`, [...g.dirtyEdges]);
742
789
  for (const edgeId of g.dirtyEdges) {
743
- log3.debug(`updating dummies of edge ${edgeId}`);
744
790
  const edge = g.getEdge(edgeId);
745
- const { type } = edge;
791
+ const { type, style } = edge;
746
792
  const sourceLayer = edge.sourceNode(g).layerIndex(g);
747
793
  const targetLayer = edge.targetNode(g).layerIndex(g);
748
794
  let segIndex = 0;
@@ -766,12 +812,10 @@ var Dummy = class _Dummy {
766
812
  });
767
813
  target = { id: dummy.id };
768
814
  }
769
- seg = Seg.add(g, { source, target, type, edgeIds: ISet4([edgeId]) });
770
- log3.debug(`edge ${edgeId}: adding segment ${seg.id} from ${source.id} at layer ${layerIndex - 1} to ${target.id} at layer ${layerIndex}`);
815
+ seg = Seg.add(g, { source, target, type, style, edgeIds: ISet4([edgeId]) });
771
816
  segs.splice(segIndex, 0, seg.id);
772
817
  changed = true;
773
818
  } else if (segLayer < layerIndex || seg.source.id != source.id || seg.source.port != source.port || layerIndex == targetLayer && (seg.target.id != edge.target.id || seg.target.port != edge.target.port)) {
774
- log3.debug(`edge ${edgeId}: removing segment ${seg.id} from layer ${layerIndex - 1} to layer ${layerIndex}`);
775
819
  seg = seg.delEdgeId(g, edgeId);
776
820
  segs.splice(segIndex, 1);
777
821
  changed = true;
@@ -783,14 +827,12 @@ var Dummy = class _Dummy {
783
827
  }
784
828
  }
785
829
  while (segIndex < segs.length) {
786
- log3.debug(`edge ${edgeId}: removing trailing segment ${segs[segIndex]}`);
787
830
  g.getSeg(segs[segIndex]).delEdgeId(g, edgeId);
788
831
  segs.splice(segIndex, 1);
789
832
  changed = true;
790
833
  segIndex++;
791
834
  }
792
835
  if (changed) {
793
- log3.debug(`edge ${edgeId}: updated segments to ${segs.join(", ")}`);
794
836
  edge.setSegIds(g, segs);
795
837
  }
796
838
  }
@@ -805,7 +847,6 @@ var Dummy = class _Dummy {
805
847
  const dir = side == "source" ? "in" : "out";
806
848
  const altSide = side == "source" ? "target" : "source";
807
849
  const altDir = altSide == "source" ? "in" : "out";
808
- log3.debug(`merging dummies by ${side}`);
809
850
  for (const layerId of layerIds) {
810
851
  let layer = g.getLayer(layerId);
811
852
  const groups = /* @__PURE__ */ new Map();
@@ -814,7 +855,7 @@ var Dummy = class _Dummy {
814
855
  const node = g.getNode(nodeId);
815
856
  if (node.isMerged) continue;
816
857
  const edge = g.getEdge(node.edgeIds[0]);
817
- const key = Edge.id(edge, "k:", side);
858
+ const key = Edge.key(edge, "k:", side);
818
859
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
819
860
  groups.get(key).add(node);
820
861
  }
@@ -856,7 +897,7 @@ var Dummy = class _Dummy {
856
897
 
857
898
  // src/graph/services/layers.ts
858
899
  import { Seq } from "immutable";
859
- var log4 = logger("layers");
900
+ var log5 = logger("layers");
860
901
  var Layers = class {
861
902
  static updateLayers(g) {
862
903
  const stack = [...g.dirtyNodes].map((id) => g.getNode(id)).filter((node) => !node.isDummy).sort((a, b) => b.layerIndex(g) - a.layerIndex(g)).map((node) => node.id);
@@ -919,13 +960,11 @@ var Layers = class {
919
960
 
920
961
  // src/graph/services/layout.ts
921
962
  import { Seq as Seq2 } from "immutable";
922
- var log5 = logger("layout");
963
+ var log6 = logger("layout");
923
964
  var Layout = class _Layout {
924
965
  static parentIndex(g, node) {
925
966
  const parents = Seq2([...node.adjs(g, "segs", "in")]);
926
- console.log(`parents of ${node.id}:`, [...parents], [...parents.map((p) => p.index)]);
927
967
  const pidx = parents.map((p) => p.index).min();
928
- log5.debug(`node ${node.id}: parent index ${pidx}`);
929
968
  if (pidx !== void 0) return pidx;
930
969
  return node.isDummy ? -Infinity : Infinity;
931
970
  }
@@ -943,7 +982,6 @@ var Layout = class _Layout {
943
982
  return minA.localeCompare(minB);
944
983
  }
945
984
  static positionNodes(g) {
946
- console.log("positionNodes", g.dirtyNodes);
947
985
  for (const nodeId of g.dirtyNodes)
948
986
  g.dirtyLayers.add(g.getNode(nodeId).layerId);
949
987
  let adjustNext = false;
@@ -951,15 +989,12 @@ var Layout = class _Layout {
951
989
  if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
952
990
  adjustNext = false;
953
991
  let layer = g.getLayer(layerId);
954
- console.log(`positioning layer ${layerId} at ${layer.index}`);
955
992
  const pidxs = /* @__PURE__ */ new Map();
956
993
  for (const nodeId of layer.nodeIds)
957
994
  pidxs.set(nodeId, _Layout.parentIndex(g, g.getNode(nodeId)));
958
- console.log("pidxs", pidxs);
959
995
  const sorted = [...layer.nodeIds].sort(
960
996
  (aId, bId) => _Layout.compareNodes(g, aId, bId, pidxs)
961
997
  );
962
- console.log(`sorted:`, sorted);
963
998
  if (layer.hasSortOrder(sorted)) continue;
964
999
  g.dirtyLayers.add(layerId);
965
1000
  layer = layer.setSorted(g, sorted);
@@ -967,7 +1002,6 @@ var Layout = class _Layout {
967
1002
  let lpos = 0;
968
1003
  for (let i = 0; i < sorted.length; i++) {
969
1004
  let node = g.getNode(sorted[i]);
970
- log5.debug(`node ${node.id}: final index ${i}`);
971
1005
  node = node.setIndex(g, i).setLayerPos(g, lpos);
972
1006
  const size = node.dims?.[g.w] ?? 0;
973
1007
  lpos += size + g.options.nodeMargin;
@@ -999,7 +1033,12 @@ var Layout = class _Layout {
999
1033
  for (const layerId of layerIds) {
1000
1034
  if (!adjustNext && !g.dirtyLayers.has(layerId)) continue;
1001
1035
  adjustNext = false;
1036
+ let iterations = 0;
1002
1037
  while (true) {
1038
+ if (++iterations > 10) {
1039
+ log6.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1040
+ break;
1041
+ }
1003
1042
  let changed = false;
1004
1043
  const nodeIds = _Layout.sortLayer(g, layerId, reverseNodes);
1005
1044
  for (const nodeId of nodeIds) {
@@ -1065,17 +1104,45 @@ var Layout = class _Layout {
1065
1104
  [g.x]: p[g.x] + w / 2,
1066
1105
  [g.y]: p[g.y] + h / 2
1067
1106
  };
1068
- p[g.x] += _Layout.nodePortOffset(g, nodeId, seg[side].port);
1069
- if (side == "source" == g.r)
1107
+ p[g.x] += _Layout.nodePortOffset(g, nodeId, seg, side);
1108
+ if (side == "target" == g.r)
1070
1109
  p[g.y] += h;
1071
1110
  return p;
1072
1111
  }
1073
- static nodePortOffset(g, nodeId, port) {
1074
- if (!port) return g.options.defaultPortOffset;
1075
- return g.options.defaultPortOffset;
1112
+ static nodePortOffset(g, nodeId, seg, side) {
1113
+ const node = g.getNode(nodeId);
1114
+ const dir = side == "source" ? "out" : "in";
1115
+ const portId = seg[side].port;
1116
+ let min = 0, size = node.dims?.[g.w] ?? 0;
1117
+ if (portId) {
1118
+ const ports = node.ports?.[dir];
1119
+ const port = ports?.find((p) => p.id === portId);
1120
+ if (port?.offset !== void 0) {
1121
+ min = port.offset;
1122
+ size = port.size ?? 0;
1123
+ }
1124
+ }
1125
+ const alt = side == "source" ? "target" : "source";
1126
+ let segs = [];
1127
+ const keyOf = (seg2) => `${seg2.type ?? ""}:${seg2[side].marker ?? ""}`;
1128
+ for (const segId of node.segs[dir])
1129
+ segs.push(g.getSeg(segId));
1130
+ if (portId) segs = segs.filter((s) => s[side].port == portId);
1131
+ const groups = Object.groupBy(segs, (s) => keyOf(s));
1132
+ const posMap = /* @__PURE__ */ new Map();
1133
+ for (const [key, segs2] of Object.entries(groups)) {
1134
+ let pos = Infinity;
1135
+ for (const seg2 of segs2) pos = Math.min(pos, seg2.node(g, alt).lpos);
1136
+ posMap.set(key, pos);
1137
+ }
1138
+ const keys = [...posMap.keys()].sort((a, b) => posMap.get(a) - posMap.get(b));
1139
+ const gap = size / (keys.length + 1);
1140
+ const index = keys.indexOf(keyOf(seg));
1141
+ return min + (index + 1) * gap;
1076
1142
  }
1077
1143
  static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1078
1144
  const node = g.getNode(nodeId);
1145
+ log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
1079
1146
  if (!conservative)
1080
1147
  _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1081
1148
  const space = g.options.nodeMargin;
@@ -1106,7 +1173,7 @@ var Layout = class _Layout {
1106
1173
  g.getNode(node.aligned[dir]).setAligned(g, alt, void 0);
1107
1174
  if (otherId)
1108
1175
  g.getNode(otherId).setAligned(g, alt, nodeId);
1109
- node.setAligned(g, dir, otherId);
1176
+ node.setAligned(g, dir, otherId).setLayerPos(g, lpos);
1110
1177
  }
1111
1178
  static *aligned(g, nodeId, dir) {
1112
1179
  const visit = function* (node2, dir2) {
@@ -1166,39 +1233,141 @@ var Layout = class _Layout {
1166
1233
  let pos = 0;
1167
1234
  const dir = g.r ? -1 : 1;
1168
1235
  const trackSep = Math.max(
1169
- g.options.layerMargin,
1170
1236
  g.options.edgeSpacing,
1171
1237
  g.options.turnRadius
1172
1238
  );
1239
+ const marginSep = Math.max(
1240
+ g.options.edgeSpacing,
1241
+ g.options.layerMargin,
1242
+ g.options.turnRadius + g.options.markerSize
1243
+ );
1173
1244
  for (const layerId of g.layerList) {
1174
1245
  let layer = g.getLayer(layerId);
1175
1246
  let height;
1176
- console.log(`getCoords: layer = ${layerId} at ${layer.index}`);
1177
1247
  if (g.dirtyLayers.has(layerId)) {
1178
1248
  height = Seq2(layer.nodes(g)).map((node) => node.dims?.[g.h] ?? 0).max() ?? 0;
1179
1249
  layer = layer.setSize(g, height);
1180
1250
  } else height = layer.size;
1181
- console.log(`getCoords: layer = ${layerId}: pos = ${pos}, height = ${height}`);
1182
1251
  for (const node of layer.nodes(g)) {
1183
1252
  if (!g.dirtyNodes.has(node.id) && pos == layer.pos) continue;
1184
1253
  const npos = { [g.x]: node.lpos, [g.y]: pos };
1185
1254
  if (!g.n) npos[g.y] += dir * height;
1186
1255
  if (g.r == g.n) npos[g.y] -= node.dims?.[g.h] ?? 0;
1187
- console.log(`getCoords: node = ${node.id}: pos:`, npos);
1188
1256
  node.setPos(g, npos);
1189
1257
  }
1190
1258
  layer = layer.setPos(g, pos);
1191
- pos += dir * (height + trackSep);
1259
+ pos += dir * (height + marginSep);
1192
1260
  for (const track of layer.tracks) {
1193
1261
  for (const segId of track)
1194
1262
  g.getSeg(segId).setTrackPos(g, pos);
1195
- pos += dir * g.options.edgeSpacing;
1263
+ pos += dir * trackSep;
1196
1264
  }
1265
+ pos += dir * (marginSep - trackSep);
1197
1266
  }
1198
1267
  }
1199
1268
  };
1200
1269
 
1270
+ // src/canvas/marker.tsx
1271
+ import { default as default2 } from "./marker.css?raw";
1272
+ import { jsx } from "jsx-dom/jsx-runtime";
1273
+ function arrow(size, classPrefix, reverse = false) {
1274
+ const h = size / 1.5;
1275
+ const w = size;
1276
+ const ry = h / 2;
1277
+ const suffix = reverse ? "-reverse" : "";
1278
+ return /* @__PURE__ */ jsx(
1279
+ "marker",
1280
+ {
1281
+ id: `g3p-marker-arrow${suffix}`,
1282
+ className: `${classPrefix}-marker ${classPrefix}-marker-arrow`,
1283
+ markerWidth: size,
1284
+ markerHeight: size,
1285
+ refX: "2",
1286
+ refY: ry,
1287
+ orient: reverse ? "auto-start-reverse" : "auto",
1288
+ markerUnits: "userSpaceOnUse",
1289
+ children: /* @__PURE__ */ jsx("path", { d: `M0,0 L0,${h} L${w},${ry} z` })
1290
+ }
1291
+ );
1292
+ }
1293
+ function circle(size, classPrefix, reverse = false) {
1294
+ const r = size / 3;
1295
+ const cy = size / 2;
1296
+ const suffix = reverse ? "-reverse" : "";
1297
+ return /* @__PURE__ */ jsx(
1298
+ "marker",
1299
+ {
1300
+ id: `g3p-marker-circle${suffix}`,
1301
+ className: `${classPrefix}-marker ${classPrefix}-marker-circle`,
1302
+ markerWidth: size,
1303
+ markerHeight: size,
1304
+ refX: "2",
1305
+ refY: cy,
1306
+ orient: reverse ? "auto-start-reverse" : "auto",
1307
+ markerUnits: "userSpaceOnUse",
1308
+ children: /* @__PURE__ */ jsx("circle", { cx: r + 2, cy, r })
1309
+ }
1310
+ );
1311
+ }
1312
+ function diamond(size, classPrefix, reverse = false) {
1313
+ const w = size * 0.7;
1314
+ const h = size / 2;
1315
+ const cy = size / 2;
1316
+ const suffix = reverse ? "-reverse" : "";
1317
+ return /* @__PURE__ */ jsx(
1318
+ "marker",
1319
+ {
1320
+ id: `g3p-marker-diamond${suffix}`,
1321
+ className: `${classPrefix}-marker ${classPrefix}-marker-diamond`,
1322
+ markerWidth: size,
1323
+ markerHeight: size,
1324
+ refX: "2",
1325
+ refY: cy,
1326
+ orient: reverse ? "auto-start-reverse" : "auto",
1327
+ markerUnits: "userSpaceOnUse",
1328
+ children: /* @__PURE__ */ jsx("path", { d: `M2,${cy} L${2 + w / 2},${cy - h / 2} L${2 + w},${cy} L${2 + w / 2},${cy + h / 2} z` })
1329
+ }
1330
+ );
1331
+ }
1332
+ function bar(size, classPrefix, reverse = false) {
1333
+ const h = size * 0.6;
1334
+ const cy = size / 2;
1335
+ const suffix = reverse ? "-reverse" : "";
1336
+ return /* @__PURE__ */ jsx(
1337
+ "marker",
1338
+ {
1339
+ id: `g3p-marker-bar${suffix}`,
1340
+ className: `${classPrefix}-marker ${classPrefix}-marker-bar`,
1341
+ markerWidth: size,
1342
+ markerHeight: size,
1343
+ refX: "2",
1344
+ refY: cy,
1345
+ orient: reverse ? "auto-start-reverse" : "auto",
1346
+ markerUnits: "userSpaceOnUse",
1347
+ children: /* @__PURE__ */ jsx("line", { x1: "2", y1: cy - h / 2, x2: "2", y2: cy + h / 2, "stroke-width": "2" })
1348
+ }
1349
+ );
1350
+ }
1351
+ function none(size, classPrefix, reverse = false) {
1352
+ return void 0;
1353
+ }
1354
+ function normalize(data) {
1355
+ let source = data.source?.marker ?? data.style?.marker?.source;
1356
+ let target = data.target?.marker ?? data.style?.marker?.target ?? "arrow";
1357
+ if (source == "none") source = void 0;
1358
+ if (target == "none") target = void 0;
1359
+ return { source, target };
1360
+ }
1361
+ var markerDefs = {
1362
+ arrow,
1363
+ circle,
1364
+ diamond,
1365
+ bar,
1366
+ none
1367
+ };
1368
+
1201
1369
  // src/graph/services/lines.ts
1370
+ var log7 = logger("lines");
1202
1371
  var Lines = class _Lines {
1203
1372
  static layoutSeg(g, seg) {
1204
1373
  const sourcePos = Layout.anchorPos(g, seg, "source");
@@ -1273,7 +1442,12 @@ var Lines = class _Lines {
1273
1442
  const radius = g.options.turnRadius;
1274
1443
  const p1 = Layout.anchorPos(g, seg, "source");
1275
1444
  const p2 = Layout.anchorPos(g, seg, "target");
1276
- const path = seg.trackPos !== void 0 ? _Lines.createRailroadPath(g, p1, p2, seg.trackPos, radius) : _Lines.createDirectPath(g, p1, p2, radius);
1445
+ const source = seg.sourceNode(g);
1446
+ const target = seg.targetNode(g);
1447
+ const marker = normalize(seg);
1448
+ if (source.isDummy) marker.source = void 0;
1449
+ if (target.isDummy) marker.target = void 0;
1450
+ const path = seg.trackPos !== void 0 ? _Lines.createRailroadPath(g, p1, p2, seg.trackPos, radius, marker) : _Lines.createDirectPath(g, p1, p2, radius, marker);
1277
1451
  const svg = _Lines.pathToSVG(path);
1278
1452
  seg.setSVG(g, svg);
1279
1453
  }
@@ -1299,7 +1473,7 @@ var Lines = class _Lines {
1299
1473
  }
1300
1474
  return line;
1301
1475
  }
1302
- static pathBuilder(g, start, end, trackPos, radius) {
1476
+ static pathBuilder(g, start, end, trackPos, radius, marker) {
1303
1477
  const { x, y } = g;
1304
1478
  const lr = end[x] > start[x];
1305
1479
  const d = lr ? 1 : -1;
@@ -1311,6 +1485,8 @@ var Lines = class _Lines {
1311
1485
  if (g.r) s = 1 - s;
1312
1486
  if (!lr) s = 1 - s;
1313
1487
  if (!g.v) s = 1 - s;
1488
+ if (marker.source) start[y] += o * (g.options.markerSize - 1);
1489
+ if (marker.target) end[y] -= o * (g.options.markerSize - 1);
1314
1490
  const p = { ...start, s };
1315
1491
  const path = [];
1316
1492
  const advance = (p2, type) => {
@@ -1319,8 +1495,8 @@ var Lines = class _Lines {
1319
1495
  return { x, y, lr, d, o, rd, ro, t, s, p, path, advance };
1320
1496
  }
1321
1497
  // Create a railroad-style path with two 90-degree turns
1322
- static createRailroadPath(g, start, end, trackPos, radius) {
1323
- const { x, y, rd, ro, t, s, p, path, advance } = this.pathBuilder(g, start, end, trackPos, radius);
1498
+ static createRailroadPath(g, start, end, trackPos, radius, marker) {
1499
+ const { x, y, rd, ro, t, s, p, path, advance } = this.pathBuilder(g, start, end, trackPos, radius, marker);
1324
1500
  advance({ [y]: t - ro }, "line");
1325
1501
  advance({ [x]: p[x] + rd, [y]: t }, "arc");
1326
1502
  advance({ [x]: end[x] - rd }, "line");
@@ -1329,8 +1505,8 @@ var Lines = class _Lines {
1329
1505
  return path;
1330
1506
  }
1331
1507
  // Create a mostly-vertical path with optional S-curve
1332
- static createDirectPath(g, start, end, radius) {
1333
- const { x, y, d, o, s, p, path, advance } = this.pathBuilder(g, start, end, 0, radius);
1508
+ static createDirectPath(g, start, end, radius, marker) {
1509
+ const { x, y, d, o, s, p, path, advance } = this.pathBuilder(g, start, end, 0, radius, marker);
1334
1510
  const dx = Math.abs(end.x - start.x);
1335
1511
  const dy = Math.abs(end.y - start.y);
1336
1512
  const d_ = { x: dx, y: dy };
@@ -1421,21 +1597,14 @@ var Lines = class _Lines {
1421
1597
  }
1422
1598
  };
1423
1599
 
1424
- // src/graph/types/graph.ts
1425
- var defaultOptions = {
1426
- mergeOrder: ["target", "source"],
1427
- nodeMargin: 15,
1428
- dummyNodeSize: 15,
1429
- defaultPortOffset: 20,
1430
- nodeAlign: "natural",
1431
- edgeSpacing: 10,
1432
- turnRadius: 10,
1433
- orientation: "TB",
1434
- layerMargin: 5,
1435
- alignIterations: 5,
1436
- alignThreshold: 10,
1437
- separateTrackSets: true,
1438
- layoutSteps: null
1600
+ // src/graph/graph.ts
1601
+ var emptyChanges = {
1602
+ addedNodes: [],
1603
+ removedNodes: [],
1604
+ updatedNodes: [],
1605
+ addedEdges: [],
1606
+ removedEdges: [],
1607
+ updatedEdges: []
1439
1608
  };
1440
1609
  var Graph = class _Graph {
1441
1610
  prior;
@@ -1464,30 +1633,10 @@ var Graph = class _Graph {
1464
1633
  x;
1465
1634
  y;
1466
1635
  d;
1467
- constructor({ prior, changes, options, nodes, edges } = {}) {
1636
+ constructor({ prior, changes, options }) {
1637
+ this.options = prior?.options ?? options;
1638
+ this.changes = changes ?? emptyChanges;
1468
1639
  this.initFromPrior(prior);
1469
- this.dirtyNodes = /* @__PURE__ */ new Set();
1470
- this.dirtyEdges = /* @__PURE__ */ new Set();
1471
- this.dirtyLayers = /* @__PURE__ */ new Set();
1472
- this.dirtySegs = /* @__PURE__ */ new Set();
1473
- this.delNodes = /* @__PURE__ */ new Set();
1474
- this.delEdges = /* @__PURE__ */ new Set();
1475
- this.delSegs = /* @__PURE__ */ new Set();
1476
- this.options = {
1477
- ...defaultOptions,
1478
- ...prior?.options,
1479
- ...options
1480
- };
1481
- this.changes = changes ?? {
1482
- addedNodes: [],
1483
- removedNodes: [],
1484
- updatedNodes: [],
1485
- addedEdges: [],
1486
- removedEdges: []
1487
- };
1488
- this.changes.addedNodes.push(...nodes || []);
1489
- this.changes.addedEdges.push(...edges || []);
1490
- this.dirty = this.changes.addedNodes.length > 0 || this.changes.removedNodes.length > 0 || this.changes.addedEdges.length > 0 || this.changes.removedEdges.length > 0;
1491
1640
  this.r = this.options.orientation === "BT" || this.options.orientation === "RL";
1492
1641
  this.v = this.options.orientation === "TB" || this.options.orientation === "BT";
1493
1642
  this.h = this.v ? "h" : "w";
@@ -1503,38 +1652,41 @@ var Graph = class _Graph {
1503
1652
  this.n = true;
1504
1653
  else
1505
1654
  this.n = natAligns[this.options.orientation] == this.options.nodeAlign;
1506
- if (this.dirty) {
1507
- try {
1508
- this.beginMutate();
1509
- this.applyChanges();
1510
- Cycles.checkCycles(this);
1511
- Layers.updateLayers(this);
1512
- Dummy.updateDummies(this);
1513
- Dummy.mergeDummies(this);
1514
- Layout.positionNodes(this);
1515
- Layout.alignAll(this);
1516
- Lines.trackEdges(this);
1517
- Layout.getCoords(this);
1518
- Lines.pathEdges(this);
1519
- } catch (e) {
1520
- this.initFromPrior(this.prior);
1521
- throw e;
1522
- } finally {
1523
- this.endMutate();
1524
- }
1655
+ if (this.dirty) this.processUpdate();
1656
+ }
1657
+ processUpdate() {
1658
+ try {
1659
+ this.beginMutate();
1660
+ this.applyChanges();
1661
+ Cycles.checkCycles(this);
1662
+ Layers.updateLayers(this);
1663
+ Dummy.updateDummies(this);
1664
+ Dummy.mergeDummies(this);
1665
+ Layout.positionNodes(this);
1666
+ Layout.alignAll(this);
1667
+ Lines.trackEdges(this);
1668
+ Layout.getCoords(this);
1669
+ Lines.pathEdges(this);
1670
+ } catch (e) {
1671
+ this.initFromPrior(this.prior);
1672
+ throw e;
1673
+ } finally {
1674
+ this.endMutate();
1525
1675
  }
1526
1676
  }
1527
1677
  applyChanges() {
1528
1678
  for (const edge of this.changes.removedEdges)
1529
- this.getEdge(Edge.id(edge)).delSelf(this);
1679
+ Edge.del(this, edge);
1530
1680
  for (const node of this.changes.removedNodes)
1531
- this.getNode(node.id).delSelf(this);
1681
+ Node.del(this, node);
1532
1682
  for (const node of this.changes.addedNodes)
1533
1683
  Node.addNormal(this, node);
1534
1684
  for (const edge of this.changes.addedEdges)
1535
1685
  Edge.add(this, edge);
1536
1686
  for (const node of this.changes.updatedNodes)
1537
- this.dirtyNodes.add(node.id);
1687
+ Node.update(this, node);
1688
+ for (const edge of this.changes.updatedEdges)
1689
+ Edge.update(this, edge);
1538
1690
  }
1539
1691
  layerAt(index) {
1540
1692
  while (index >= this.layerList.size)
@@ -1611,24 +1763,24 @@ var Graph = class _Graph {
1611
1763
  nodes.forEach((node) => mutator.addNode(node));
1612
1764
  });
1613
1765
  }
1614
- addEdges(...edges) {
1766
+ removeNodes(...nodes) {
1615
1767
  return this.withMutations((mutator) => {
1616
- edges.forEach((edge) => mutator.addEdge(edge));
1768
+ nodes.forEach((node) => mutator.removeNode(node));
1617
1769
  });
1618
1770
  }
1619
- addEdge(edge) {
1771
+ removeNode(node) {
1620
1772
  return this.withMutations((mutator) => {
1621
- mutator.addEdge(edge);
1773
+ mutator.removeNode(node);
1622
1774
  });
1623
1775
  }
1624
- removeNodes(...nodes) {
1776
+ addEdges(...edges) {
1625
1777
  return this.withMutations((mutator) => {
1626
- nodes.forEach((node) => mutator.removeNode(node));
1778
+ edges.forEach((edge) => mutator.addEdge(edge));
1627
1779
  });
1628
1780
  }
1629
- removeNode(node) {
1781
+ addEdge(edge) {
1630
1782
  return this.withMutations((mutator) => {
1631
- mutator.removeNode(node);
1783
+ mutator.addEdge(edge);
1632
1784
  });
1633
1785
  }
1634
1786
  removeEdges(...edges) {
@@ -1678,6 +1830,14 @@ var Graph = class _Graph {
1678
1830
  this.nextLayerId = prior?.nextLayerId ?? 0;
1679
1831
  this.nextDummyId = prior?.nextDummyId ?? 0;
1680
1832
  this.prior = prior;
1833
+ this.dirtyNodes = /* @__PURE__ */ new Set();
1834
+ this.dirtyEdges = /* @__PURE__ */ new Set();
1835
+ this.dirtyLayers = /* @__PURE__ */ new Set();
1836
+ this.dirtySegs = /* @__PURE__ */ new Set();
1837
+ this.delNodes = /* @__PURE__ */ new Set();
1838
+ this.delEdges = /* @__PURE__ */ new Set();
1839
+ this.delSegs = /* @__PURE__ */ new Set();
1840
+ this.dirty = this.changes.addedNodes.length > 0 || this.changes.removedNodes.length > 0 || this.changes.updatedNodes.length > 0 || this.changes.addedEdges.length > 0 || this.changes.removedEdges.length > 0;
1681
1841
  }
1682
1842
  beginMutate() {
1683
1843
  this.nodes = this.nodes.asMutable();
@@ -1707,15 +1867,20 @@ var Graph = class _Graph {
1707
1867
  }
1708
1868
  };
1709
1869
 
1710
- // src/canvas/canvas.css
1711
- 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}";
1870
+ // src/common.ts
1871
+ var screenPos = (x, y) => ({ x, y });
1872
+ var canvasPos = (x, y) => ({ x, y });
1873
+ var graphPos = (x, y) => ({ x, y });
1874
+
1875
+ // src/canvas/node.tsx
1876
+ import styles from "./node.css?raw";
1712
1877
 
1713
1878
  // src/canvas/styler.ts
1714
1879
  var injected = {};
1715
- function styler(name, styles, prefix) {
1880
+ function styler(name, styles4, prefix) {
1716
1881
  if (prefix === "g3p" && !injected[name]) {
1717
1882
  const style = document.createElement("style");
1718
- style.textContent = styles;
1883
+ style.textContent = styles4;
1719
1884
  document.head.appendChild(style);
1720
1885
  injected[name] = true;
1721
1886
  }
@@ -1727,98 +1892,106 @@ function styler(name, styles, prefix) {
1727
1892
  };
1728
1893
  }
1729
1894
 
1730
- // src/canvas/node.css
1731
- 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}";
1732
-
1733
1895
  // src/canvas/node.tsx
1734
- import { Fragment, jsx as jsx2, jsxs } from "jsx-dom/jsx-runtime";
1896
+ import { jsx as jsx2, jsxs } from "jsx-dom/jsx-runtime";
1897
+ var log8 = logger("canvas");
1735
1898
  var Node2 = class {
1736
1899
  selected;
1737
1900
  hovered;
1738
1901
  container;
1739
- dims;
1740
1902
  content;
1741
- measured;
1903
+ canvas;
1904
+ data;
1905
+ classPrefix;
1742
1906
  isDummy;
1743
- constructor(options) {
1744
- this.isDummy = false;
1745
- Object.assign(this, {
1746
- selected: false,
1747
- hovered: false,
1748
- renderNode: () => null,
1749
- onClick: () => null,
1750
- onMouseEnter: () => null,
1751
- onMouseLeave: () => null,
1752
- onContextMenu: () => null,
1753
- onMouseDown: () => null,
1754
- onMouseUp: () => null,
1755
- classPrefix: "g3p",
1756
- ...options
1757
- });
1758
- if (!this.isDummy) {
1759
- this.content = this.renderNode(this.data);
1760
- this.measured = false;
1907
+ pos;
1908
+ constructor(canvas, data, isDummy = false) {
1909
+ this.canvas = canvas;
1910
+ this.data = data;
1911
+ this.selected = false;
1912
+ this.hovered = false;
1913
+ this.classPrefix = canvas.classPrefix;
1914
+ this.isDummy = isDummy;
1915
+ if (this.isDummy) {
1916
+ const size = canvas.dummyNodeSize;
1761
1917
  } else {
1762
- this.measured = true;
1918
+ const render = data.render ?? canvas.renderNode;
1919
+ this.content = this.renderContent(render(data.data));
1763
1920
  }
1764
1921
  }
1765
- getSize() {
1766
- const rect = this.content.getBoundingClientRect();
1767
- this.dims = { w: rect.width, h: rect.height };
1768
- this.measured = true;
1922
+ remove() {
1923
+ this.container.remove();
1924
+ }
1925
+ append() {
1926
+ console.log("append", this);
1927
+ this.canvas.group.appendChild(this.container);
1928
+ }
1929
+ needsContentSize() {
1930
+ return !this.isDummy && this.content instanceof HTMLElement;
1931
+ }
1932
+ needsContainerSize() {
1933
+ return !this.isDummy;
1769
1934
  }
1770
1935
  handleClick(e) {
1771
1936
  e.stopPropagation();
1772
- this.onClick?.(this.data, e);
1773
1937
  }
1774
1938
  handleMouseEnter(e) {
1775
- this.onMouseEnter?.(this.data, e);
1776
1939
  }
1777
1940
  handleMouseLeave(e) {
1778
- this.onMouseLeave?.(this.data, e);
1779
1941
  }
1780
1942
  handleContextMenu(e) {
1781
- if (this.onContextMenu) {
1782
- e.stopPropagation();
1783
- this.onContextMenu(this.data, e);
1784
- }
1785
1943
  }
1786
1944
  handleMouseDown(e) {
1787
- this.onMouseDown?.(this.data, e);
1788
1945
  }
1789
1946
  handleMouseUp(e) {
1790
- this.onMouseUp?.(this.data, e);
1791
1947
  }
1792
1948
  setPos(pos) {
1793
- console.log(`setPos:`, this, pos);
1794
1949
  this.pos = pos;
1795
- this.container.setAttribute("transform", `translate(${this.pos.x}, ${this.pos.y})`);
1796
- }
1797
- // render will be called once the node is measured
1798
- render() {
1799
- const c = styler("node", node_default, this.classPrefix);
1800
- return /* @__PURE__ */ jsx2(
1950
+ const { x, y } = pos;
1951
+ this.container.setAttribute("transform", `translate(${x}, ${y})`);
1952
+ }
1953
+ hasPorts() {
1954
+ return !!this.data?.ports?.in?.length || !!this.data?.ports?.out?.length;
1955
+ }
1956
+ renderContent(el) {
1957
+ const hasPorts = this.hasPorts();
1958
+ el = this.renderBorder(el);
1959
+ if (hasPorts)
1960
+ el = this.renderOutsidePorts(el);
1961
+ return el;
1962
+ }
1963
+ renderContainer() {
1964
+ const c = styler("node", styles, this.classPrefix);
1965
+ const hasPorts = this.hasPorts();
1966
+ const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
1967
+ const nodeType = this.data?.type;
1968
+ const typeClass = nodeType ? `g3p-node-type-${nodeType}` : "";
1969
+ this.container = /* @__PURE__ */ jsx2(
1801
1970
  "g",
1802
1971
  {
1803
- ref: (el) => this.container = el,
1804
- className: `${c("container")} ${c("dummy", this.isDummy)}`,
1805
- onClick: this.handleClick.bind(this),
1806
- onMouseEnter: this.handleMouseEnter.bind(this),
1807
- onMouseLeave: this.handleMouseLeave.bind(this),
1808
- onContextMenu: this.handleContextMenu.bind(this),
1809
- onMouseDown: this.handleMouseDown.bind(this),
1810
- onMouseUp: this.handleMouseUp.bind(this),
1972
+ className: `${c("container")} ${c("dummy", this.isDummy)} ${typeClass}`.trim(),
1973
+ onClick: (e) => this.handleClick(e),
1974
+ onMouseEnter: (e) => this.handleMouseEnter(e),
1975
+ onMouseLeave: (e) => this.handleMouseLeave(e),
1976
+ onContextMenu: (e) => this.handleContextMenu(e),
1977
+ onMouseDown: (e) => this.handleMouseDown(e),
1978
+ onMouseUp: (e) => this.handleMouseUp(e),
1811
1979
  style: { cursor: "pointer" },
1812
- children: this.isDummy ? this.renderDummy() : this.renderContent()
1980
+ children: inner
1813
1981
  }
1814
1982
  );
1815
1983
  }
1984
+ renderForeign() {
1985
+ const { w, h } = this.data.dims;
1986
+ return /* @__PURE__ */ jsx2("foreignObject", { width: w, height: h, children: this.content });
1987
+ }
1816
1988
  renderDummy() {
1817
- const c = styler("node", node_default, this.classPrefix);
1818
- let { w, h } = this.dims;
1989
+ const c = styler("node", styles, this.classPrefix);
1990
+ let w = this.canvas.dummyNodeSize;
1991
+ let h = this.canvas.dummyNodeSize;
1819
1992
  w /= 2;
1820
1993
  h /= 2;
1821
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1994
+ return /* @__PURE__ */ jsxs("g", { children: [
1822
1995
  /* @__PURE__ */ jsx2(
1823
1996
  "ellipse",
1824
1997
  {
@@ -1837,157 +2010,190 @@ var Node2 = class {
1837
2010
  rx: w,
1838
2011
  ry: h,
1839
2012
  fill: "none",
1840
- className: c("border"),
1841
- strokeWidth: "2"
2013
+ className: c("border")
1842
2014
  }
1843
2015
  )
1844
2016
  ] });
1845
2017
  }
1846
- renderContent() {
1847
- const c = styler("node", node_default, this.classPrefix);
1848
- const { w, h } = this.dims;
1849
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1850
- /* @__PURE__ */ jsx2(
1851
- "rect",
1852
- {
1853
- className: c("background"),
1854
- width: w,
1855
- height: h,
1856
- rx: 8,
1857
- ry: 8
1858
- }
1859
- ),
1860
- /* @__PURE__ */ jsx2(
1861
- "rect",
1862
- {
1863
- className: c("border"),
1864
- width: w,
1865
- height: h,
1866
- rx: 8,
1867
- ry: 8,
1868
- fill: "none",
1869
- strokeWidth: "2"
1870
- }
1871
- ),
1872
- /* @__PURE__ */ jsx2(
1873
- "foreignObject",
1874
- {
1875
- width: w,
1876
- height: h,
1877
- className: c("content-wrapper"),
1878
- children: /* @__PURE__ */ jsx2(
1879
- "div",
1880
- {
1881
- className: c("content"),
1882
- style: {
1883
- width: `${w}px`,
1884
- height: `${h}px`,
1885
- overflow: "hidden"
1886
- },
1887
- children: this.content
1888
- }
1889
- )
2018
+ measure(isVertical) {
2019
+ const rect = this.content.getBoundingClientRect();
2020
+ const data = this.data;
2021
+ data.dims = { w: rect.width, h: rect.height };
2022
+ for (const dir of ["in", "out"]) {
2023
+ const ports = data.ports?.[dir];
2024
+ if (!ports) continue;
2025
+ for (const port of ports) {
2026
+ const el = this.content.querySelector(`#g3p-port-${data.id}-${port.id}`);
2027
+ if (!el) continue;
2028
+ const portRect = el.getBoundingClientRect();
2029
+ if (isVertical) {
2030
+ port.offset = portRect.left - rect.left;
2031
+ port.size = portRect.width;
2032
+ } else {
2033
+ port.offset = portRect.top - rect.top;
2034
+ port.size = portRect.height;
1890
2035
  }
1891
- )
2036
+ }
2037
+ }
2038
+ }
2039
+ getPortPosition(dir) {
2040
+ const o = this.canvas.orientation;
2041
+ if (dir === "in") {
2042
+ if (o === "TB") return "top";
2043
+ if (o === "BT") return "bottom";
2044
+ if (o === "LR") return "left";
2045
+ return "right";
2046
+ } else {
2047
+ if (o === "TB") return "bottom";
2048
+ if (o === "BT") return "top";
2049
+ if (o === "LR") return "right";
2050
+ return "left";
2051
+ }
2052
+ }
2053
+ isVerticalOrientation() {
2054
+ const o = this.canvas.orientation;
2055
+ return o === "TB" || o === "BT";
2056
+ }
2057
+ isReversedOrientation() {
2058
+ const o = this.canvas.orientation;
2059
+ return o === "BT" || o === "RL";
2060
+ }
2061
+ renderPortRow(dir, inout) {
2062
+ const ports = this.data?.ports?.[dir];
2063
+ if (!ports?.length) return null;
2064
+ const c = styler("node", styles, this.classPrefix);
2065
+ const pos = this.getPortPosition(dir);
2066
+ const isVertical = this.isVerticalOrientation();
2067
+ const layoutClass = isVertical ? "row" : "col";
2068
+ const rotateLabels = false;
2069
+ const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2070
+ return /* @__PURE__ */ jsx2("div", { className: `${c("ports")} ${c(`ports-${layoutClass}`)}`, children: ports.map((port) => /* @__PURE__ */ jsx2(
2071
+ "div",
2072
+ {
2073
+ id: `g3p-port-${this.data.id}-${port.id}`,
2074
+ className: `${c("port")} ${c(`port-${inout}-${pos}`)} ${c(rotateClass)}`,
2075
+ children: port.label ?? port.id
2076
+ }
2077
+ )) });
2078
+ }
2079
+ renderInsidePorts(el) {
2080
+ const c = styler("node", styles, this.classPrefix);
2081
+ const isVertical = this.isVerticalOrientation();
2082
+ const isReversed = this.isReversedOrientation();
2083
+ let inPorts = this.renderPortRow("in", "in");
2084
+ let outPorts = this.renderPortRow("out", "in");
2085
+ if (!inPorts && !outPorts) return el;
2086
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2087
+ const wrapperClass = isVertical ? "v" : "h";
2088
+ return /* @__PURE__ */ jsxs("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2089
+ inPorts,
2090
+ el,
2091
+ outPorts
1892
2092
  ] });
1893
2093
  }
2094
+ renderOutsidePorts(el) {
2095
+ const c = styler("node", styles, this.classPrefix);
2096
+ const isVertical = this.isVerticalOrientation();
2097
+ const isReversed = this.isReversedOrientation();
2098
+ let inPorts = this.renderPortRow("in", "out");
2099
+ let outPorts = this.renderPortRow("out", "out");
2100
+ if (!inPorts && !outPorts) return el;
2101
+ if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2102
+ const wrapperClass = isVertical ? "v" : "h";
2103
+ return /* @__PURE__ */ jsxs("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2104
+ inPorts,
2105
+ el,
2106
+ outPorts
2107
+ ] });
2108
+ }
2109
+ renderBorder(el) {
2110
+ const c = styler("node", styles, this.classPrefix);
2111
+ return /* @__PURE__ */ jsx2("div", { className: c("border"), children: el });
2112
+ }
1894
2113
  };
1895
2114
 
1896
- // src/canvas/seg.css
1897
- 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}";
1898
-
1899
2115
  // src/canvas/seg.tsx
2116
+ import styles2 from "./seg.css?raw";
1900
2117
  import { jsx as jsx3, jsxs as jsxs2 } from "jsx-dom/jsx-runtime";
2118
+ var log9 = logger("canvas");
1901
2119
  var Seg2 = class {
2120
+ id;
1902
2121
  selected;
1903
2122
  hovered;
1904
- constructor(options) {
2123
+ canvas;
2124
+ classPrefix;
2125
+ type;
2126
+ svg;
2127
+ el;
2128
+ source;
2129
+ target;
2130
+ constructor(canvas, data, g) {
2131
+ this.id = data.id;
2132
+ this.canvas = canvas;
1905
2133
  this.selected = false;
1906
2134
  this.hovered = false;
1907
- Object.assign(this, {
1908
- onClick: () => {
1909
- },
1910
- onMouseEnter: () => {
1911
- },
1912
- onMouseLeave: () => {
1913
- },
1914
- onContextMenu: () => {
1915
- },
1916
- classPrefix: "g3p",
1917
- ...options
1918
- });
1919
- this.attrs ??= {};
1920
- this.attrs.targetTerminal ??= "arrow";
2135
+ this.svg = data.svg;
2136
+ this.classPrefix = canvas.classPrefix;
2137
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2138
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2139
+ this.type = data.type;
2140
+ this.el = this.render();
1921
2141
  }
1922
2142
  handleClick(e) {
1923
2143
  e.stopPropagation();
1924
- this.onClick?.(this.edgeData, e);
1925
2144
  }
1926
2145
  handleMouseEnter(e) {
1927
- this.onMouseEnter?.(this.edgeData, e);
1928
2146
  }
1929
2147
  handleMouseLeave(e) {
1930
- this.onMouseLeave?.(this.edgeData, e);
1931
2148
  }
1932
2149
  handleContextMenu(e) {
1933
- if (this.onContextMenu) {
1934
- e.stopPropagation();
1935
- this.onContextMenu(this.edgeData, e);
1936
- }
1937
2150
  }
1938
- renderTerminals() {
1939
- return {
1940
- source: this.renderTerminal(this.attrs.sourceTerminal, "source"),
1941
- target: this.renderTerminal(this.attrs.targetTerminal, "target")
1942
- };
2151
+ append() {
2152
+ this.canvas.group.appendChild(this.el);
1943
2153
  }
1944
- setSVG(svg) {
1945
- this.svg = svg;
1946
- const n = this.el.childElementCount;
1947
- this.el.childNodes[n - 2].setAttribute("d", svg);
1948
- this.el.childNodes[n - 1].setAttribute("d", svg);
2154
+ remove() {
2155
+ this.el.remove();
2156
+ }
2157
+ update(data) {
2158
+ this.svg = data.svg;
2159
+ this.type = data.type;
2160
+ this.source = data.source;
2161
+ this.target = data.target;
2162
+ this.remove();
2163
+ this.el = this.render();
2164
+ this.append();
1949
2165
  }
1950
2166
  render() {
1951
- const c = styler("edge", seg_default, this.classPrefix);
1952
- const styleAttrs = {
1953
- stroke: this.attrs.color,
1954
- strokeWidth: this.attrs.width,
1955
- strokeDasharray: this.attrs.style
1956
- };
1957
- const hoverAttrs = {
1958
- ...styleAttrs,
1959
- strokeWidth: styleAttrs.strokeWidth ? Math.max(styleAttrs.strokeWidth * 3, 10) : void 0
1960
- };
1961
- const { source, target } = this.renderTerminals();
2167
+ const c = styler("seg", styles2, this.classPrefix);
2168
+ let { source, target } = normalize(this);
2169
+ if (this.source.isDummy) source = void 0;
2170
+ if (this.target.isDummy) target = void 0;
2171
+ const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
1962
2172
  return /* @__PURE__ */ jsxs2(
1963
2173
  "g",
1964
2174
  {
1965
2175
  ref: (el) => this.el = el,
1966
- id: `g3p-seg-${this.segId}`,
1967
- className: c("container"),
2176
+ id: `g3p-seg-${this.id}`,
2177
+ className: `${c("container")} ${typeClass}`.trim(),
1968
2178
  onClick: this.handleClick.bind(this),
1969
2179
  onMouseEnter: this.handleMouseEnter.bind(this),
1970
2180
  onMouseLeave: this.handleMouseLeave.bind(this),
1971
2181
  onContextMenu: this.handleContextMenu.bind(this),
1972
2182
  children: [
1973
- source?.defs,
1974
- target?.defs,
1975
2183
  /* @__PURE__ */ jsx3(
1976
2184
  "path",
1977
2185
  {
1978
2186
  d: this.svg,
1979
- ...styleAttrs,
1980
2187
  fill: "none",
1981
2188
  className: c("line"),
1982
- markerStart: source ? `url(#${source.id})` : void 0,
1983
- markerEnd: target ? `url(#${target.id})` : void 0
2189
+ markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2190
+ markerEnd: target ? `url(#g3p-marker-${target})` : void 0
1984
2191
  }
1985
2192
  ),
1986
2193
  /* @__PURE__ */ jsx3(
1987
2194
  "path",
1988
2195
  {
1989
2196
  d: this.svg,
1990
- ...hoverAttrs,
1991
2197
  stroke: "transparent",
1992
2198
  fill: "none",
1993
2199
  className: c("hitbox"),
@@ -1998,29 +2204,48 @@ var Seg2 = class {
1998
2204
  }
1999
2205
  );
2000
2206
  }
2001
- renderTerminal(type, side) {
2002
- if (!type)
2003
- return null;
2004
- const id = `g3p-seg-${this.segId}-${side}-${type}`;
2005
- const defs = /* @__PURE__ */ jsx3("defs", { children: /* @__PURE__ */ jsx3(
2006
- "marker",
2007
- {
2008
- id,
2009
- markerWidth: "10",
2010
- markerHeight: "10",
2011
- refX: "9",
2012
- refY: "3",
2013
- orient: "auto",
2014
- markerUnits: "userSpaceOnUse",
2015
- children: /* @__PURE__ */ jsx3("path", { d: "M0,0 L0,6 L9,3 z" })
2016
- }
2017
- ) });
2018
- return { id, defs };
2019
- }
2020
2207
  };
2021
2208
 
2022
2209
  // src/canvas/canvas.tsx
2023
- import { jsx as jsx4 } from "jsx-dom/jsx-runtime";
2210
+ import styles3 from "./canvas.css?raw";
2211
+ import zoomStyles from "./zoom.css?raw";
2212
+ import { jsx as jsx4, jsxs as jsxs3 } from "jsx-dom/jsx-runtime";
2213
+ var log10 = logger("canvas");
2214
+ var themeVarMap = {
2215
+ // Canvas
2216
+ bg: "--g3p-bg",
2217
+ shadow: "--g3p-shadow",
2218
+ // Node
2219
+ border: "--g3p-border",
2220
+ borderHover: "--g3p-border-hover",
2221
+ borderSelected: "--g3p-border-selected",
2222
+ text: "--g3p-text",
2223
+ textMuted: "--g3p-text-muted",
2224
+ // Port
2225
+ bgHover: "--g3p-port-bg-hover",
2226
+ // Edge
2227
+ color: "--g3p-edge-color"
2228
+ };
2229
+ function themeToCSS(theme, selector, prefix = "") {
2230
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
2231
+ if (!entries.length) return "";
2232
+ let css = `${selector} {
2233
+ `;
2234
+ for (const [key, value] of entries) {
2235
+ let cssVar = themeVarMap[key];
2236
+ if (key === "bg" && prefix === "node") {
2237
+ cssVar = "--g3p-bg-node";
2238
+ } else if (key === "bg" && prefix === "port") {
2239
+ cssVar = "--g3p-port-bg";
2240
+ }
2241
+ if (cssVar) {
2242
+ css += ` ${cssVar}: ${value};
2243
+ `;
2244
+ }
2245
+ }
2246
+ css += "}\n";
2247
+ return css;
2248
+ }
2024
2249
  var Canvas = class {
2025
2250
  container;
2026
2251
  root;
@@ -2028,26 +2253,27 @@ var Canvas = class {
2028
2253
  transform;
2029
2254
  bounds;
2030
2255
  measurement;
2031
- nodes;
2032
- segs;
2256
+ allNodes;
2257
+ curNodes;
2258
+ curSegs;
2033
2259
  updating;
2260
+ // Pan-zoom state
2261
+ isPanning = false;
2262
+ panStart = null;
2263
+ transformStart = null;
2264
+ panScale = null;
2265
+ zoomControls;
2034
2266
  constructor(options) {
2035
- Object.assign(this, {
2036
- renderNode,
2037
- nodeStyle: () => ({}),
2038
- edgeStyle: () => ({}),
2039
- portStyle: "outside",
2040
- classPrefix: "g3p",
2041
- width: "100%",
2042
- height: "100%",
2043
- transform: { x: 0, y: 0, scale: 1 },
2044
- bounds: { min: { x: 0, y: 0 }, max: { x: 1, y: 1 } },
2045
- ...options
2046
- });
2047
- this.nodes = /* @__PURE__ */ new Map();
2048
- this.segs = /* @__PURE__ */ new Map();
2267
+ Object.assign(this, options);
2268
+ this.allNodes = /* @__PURE__ */ new Map();
2269
+ this.curNodes = /* @__PURE__ */ new Map();
2270
+ this.curSegs = /* @__PURE__ */ new Map();
2049
2271
  this.updating = false;
2272
+ this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2273
+ this.transform = { x: 0, y: 0, scale: 1 };
2050
2274
  this.createMeasurementContainer();
2275
+ this.createCanvasContainer();
2276
+ if (this.panZoom) this.setupPanZoom();
2051
2277
  }
2052
2278
  createMeasurementContainer() {
2053
2279
  this.measurement = document.createElement("div");
@@ -2060,85 +2286,94 @@ var Canvas = class {
2060
2286
  `;
2061
2287
  document.body.appendChild(this.measurement);
2062
2288
  }
2063
- update(callback) {
2064
- this.updating = true;
2065
- callback();
2066
- this.updating = false;
2289
+ getNode(key) {
2290
+ const node = this.allNodes.get(key);
2291
+ if (!node) throw new Error(`node not found: ${key}`);
2292
+ return node;
2293
+ }
2294
+ update() {
2067
2295
  let bx0 = Infinity, by0 = Infinity;
2068
2296
  let bx1 = -Infinity, by1 = -Infinity;
2069
- for (const node of this.nodes.values()) {
2070
- const nx0 = node.pos.x, nx1 = node.pos.x + node.dims.w;
2071
- const ny0 = node.pos.y, ny1 = node.pos.y + node.dims.h;
2297
+ for (const node of this.curNodes.values()) {
2298
+ const { x, y } = node.pos;
2299
+ const { w, h } = node.data.dims;
2300
+ const nx0 = x, nx1 = x + w;
2301
+ const ny0 = y, ny1 = y + h;
2072
2302
  bx0 = Math.min(bx0, nx0);
2073
2303
  by0 = Math.min(by0, ny0);
2074
2304
  bx1 = Math.max(bx1, nx1);
2075
2305
  by1 = Math.max(by1, ny1);
2076
2306
  }
2077
2307
  this.bounds = { min: { x: bx0, y: by0 }, max: { x: bx1, y: by1 } };
2078
- console.log("bounds", this.bounds);
2079
2308
  this.root.setAttribute("viewBox", this.viewBox());
2080
2309
  }
2081
- addNode(opts) {
2082
- const node = this.nodes.get(opts.data);
2083
- if (!node) throw new Error("node not found");
2084
- if (!node.container) node.render();
2085
- node.setPos(opts.pos);
2086
- this.group.appendChild(node.container);
2087
- }
2088
- updateNode(opts) {
2089
- const node = this.nodes.get(opts.data);
2090
- if (!node) throw new Error("node not found");
2091
- node.setPos(opts.pos);
2092
- }
2093
- deleteNode(opts) {
2094
- const node = this.nodes.get(opts.data);
2095
- if (!node) throw new Error("node not found");
2096
- node.container.remove();
2097
- }
2098
- addSeg(opts) {
2099
- const seg = new Seg2(opts);
2100
- this.segs.set(seg.segId, seg);
2101
- seg.render();
2102
- this.group.appendChild(seg.el);
2103
- }
2104
- updateSeg(opts) {
2105
- const seg = this.segs.get(opts.segId);
2310
+ addNode(gnode) {
2311
+ if (this.curNodes.has(gnode.id))
2312
+ throw new Error("node already exists");
2313
+ const { key } = gnode;
2314
+ let node;
2315
+ if (gnode.isDummy) {
2316
+ node = new Node2(this, gnode, true);
2317
+ node.renderContainer();
2318
+ node.setPos(gnode.pos);
2319
+ this.allNodes.set(key, node);
2320
+ } else {
2321
+ if (!this.allNodes.has(key))
2322
+ throw new Error("node has not been measured");
2323
+ node = this.getNode(key);
2324
+ }
2325
+ this.curNodes.set(gnode.id, node);
2326
+ node.append();
2327
+ }
2328
+ updateNode(gnode) {
2329
+ if (gnode.isDummy) throw new Error("dummy node cannot be updated");
2330
+ const node = this.getNode(gnode.key);
2331
+ const cur = this.curNodes.get(gnode.id);
2332
+ if (cur) cur.remove();
2333
+ this.curNodes.set(gnode.id, node);
2334
+ node.append();
2335
+ }
2336
+ deleteNode(gnode) {
2337
+ const node = this.getNode(gnode.key);
2338
+ this.curNodes.delete(gnode.id);
2339
+ node.remove();
2340
+ }
2341
+ addSeg(gseg, g) {
2342
+ if (this.curSegs.has(gseg.id))
2343
+ throw new Error("seg already exists");
2344
+ const seg = new Seg2(this, gseg, g);
2345
+ this.curSegs.set(gseg.id, seg);
2346
+ seg.append();
2347
+ }
2348
+ updateSeg(gseg) {
2349
+ const seg = this.curSegs.get(gseg.id);
2106
2350
  if (!seg) throw new Error("seg not found");
2107
- seg.setSVG(opts.svg);
2351
+ seg.update(gseg);
2108
2352
  }
2109
- deleteSeg(opts) {
2110
- const seg = this.segs.get(opts.segId);
2353
+ deleteSeg(gseg) {
2354
+ const seg = this.curSegs.get(gseg.id);
2111
2355
  if (!seg) throw new Error("seg not found");
2112
- seg.el.remove();
2113
- this.segs.delete(seg.segId);
2356
+ this.curSegs.delete(gseg.id);
2357
+ seg.remove();
2114
2358
  }
2115
- async measure(nodes) {
2116
- const newNodes = [];
2359
+ async measureNodes(nodes) {
2360
+ const newNodes = /* @__PURE__ */ new Map();
2117
2361
  for (const data of nodes) {
2118
- if (this.nodes.has(data)) continue;
2119
- const node = new Node2({
2120
- data,
2121
- renderNode: this.renderNode,
2122
- classPrefix: this.classPrefix,
2123
- isDummy: false
2124
- });
2125
- this.nodes.set(node.data, node);
2126
- if (!node.measured) {
2127
- this.measurement.appendChild(node.content);
2128
- newNodes.push(node);
2129
- }
2362
+ const node = new Node2(this, data);
2363
+ newNodes.set(data.data, node);
2364
+ this.measurement.appendChild(node.content);
2130
2365
  }
2131
- return new Promise((resolve) => {
2132
- requestAnimationFrame(() => {
2133
- for (const node of newNodes)
2134
- node.getSize();
2135
- this.measurement.textContent = "";
2136
- resolve();
2137
- });
2138
- });
2139
- }
2140
- getDims(node) {
2141
- return this.nodes.get(node).dims;
2366
+ await new Promise(requestAnimationFrame);
2367
+ const isVertical = this.orientation === "TB" || this.orientation === "BT";
2368
+ for (const node of newNodes.values()) {
2369
+ node.measure(isVertical);
2370
+ const { id, version } = node.data;
2371
+ const key = `${id}:${version}`;
2372
+ this.allNodes.set(key, node);
2373
+ node.renderContainer();
2374
+ }
2375
+ this.measurement.innerHTML = "";
2376
+ return newNodes;
2142
2377
  }
2143
2378
  onClick(e) {
2144
2379
  console.log("click", e);
@@ -2150,17 +2385,47 @@ var Canvas = class {
2150
2385
  return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2151
2386
  }
2152
2387
  viewBox() {
2153
- 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}`;
2154
- }
2155
- render() {
2156
- const c = styler("canvas", canvas_default, this.classPrefix);
2157
- return /* @__PURE__ */ jsx4(
2388
+ const p = this.padding;
2389
+ const x = this.bounds.min.x - p;
2390
+ const y = this.bounds.min.y - p;
2391
+ const w = this.bounds.max.x - this.bounds.min.x + p * 2;
2392
+ const h = this.bounds.max.y - this.bounds.min.y + p * 2;
2393
+ return `${x} ${y} ${w} ${h}`;
2394
+ }
2395
+ generateDynamicStyles() {
2396
+ let css = "";
2397
+ const prefix = this.classPrefix;
2398
+ css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
2399
+ for (const [type, vars] of Object.entries(this.nodeTypes)) {
2400
+ css += themeToCSS(vars, `.${prefix}-node-type-${type}`, "node");
2401
+ }
2402
+ for (const [type, vars] of Object.entries(this.edgeTypes)) {
2403
+ css += themeToCSS(vars, `.${prefix}-edge-type-${type}`);
2404
+ }
2405
+ return css;
2406
+ }
2407
+ createCanvasContainer() {
2408
+ const markerStyleEl = document.createElement("style");
2409
+ markerStyleEl.textContent = default2;
2410
+ document.head.appendChild(markerStyleEl);
2411
+ const zoomStyleEl = document.createElement("style");
2412
+ zoomStyleEl.textContent = zoomStyles;
2413
+ document.head.appendChild(zoomStyleEl);
2414
+ const dynamicStyles = this.generateDynamicStyles();
2415
+ if (dynamicStyles) {
2416
+ const themeStyleEl = document.createElement("style");
2417
+ themeStyleEl.textContent = dynamicStyles;
2418
+ document.head.appendChild(themeStyleEl);
2419
+ }
2420
+ const c = styler("canvas", styles3, this.classPrefix);
2421
+ const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
2422
+ this.container = /* @__PURE__ */ jsx4(
2158
2423
  "div",
2159
2424
  {
2160
- className: c("container"),
2425
+ className: `${c("container")} ${colorModeClass}`.trim(),
2161
2426
  ref: (el) => this.container = el,
2162
2427
  onContextMenu: this.onContextMenu.bind(this),
2163
- children: /* @__PURE__ */ jsx4(
2428
+ children: /* @__PURE__ */ jsxs3(
2164
2429
  "svg",
2165
2430
  {
2166
2431
  ref: (el) => this.root = el,
@@ -2170,70 +2435,308 @@ var Canvas = class {
2170
2435
  viewBox: this.viewBox(),
2171
2436
  preserveAspectRatio: "xMidYMid meet",
2172
2437
  onClick: this.onClick.bind(this),
2173
- children: /* @__PURE__ */ jsx4(
2174
- "g",
2175
- {
2176
- ref: (el) => this.group = el,
2177
- transform: this.groupTransform()
2178
- }
2179
- )
2438
+ children: [
2439
+ /* @__PURE__ */ jsxs3("defs", { children: [
2440
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, false)),
2441
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, true))
2442
+ ] }),
2443
+ /* @__PURE__ */ jsx4(
2444
+ "g",
2445
+ {
2446
+ ref: (el) => this.group = el,
2447
+ transform: this.groupTransform()
2448
+ }
2449
+ )
2450
+ ]
2180
2451
  }
2181
2452
  )
2182
2453
  }
2183
2454
  );
2184
2455
  }
2456
+ // ==================== Pan-Zoom ====================
2457
+ setupPanZoom() {
2458
+ this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false });
2459
+ this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
2460
+ document.addEventListener("mousemove", this.onMouseMove.bind(this));
2461
+ document.addEventListener("mouseup", this.onMouseUp.bind(this));
2462
+ this.createZoomControls();
2463
+ }
2464
+ /** Convert screen coordinates to canvas-relative coordinates */
2465
+ screenToCanvas(screen) {
2466
+ const rect = this.container.getBoundingClientRect();
2467
+ return canvasPos(screen.x - rect.left, screen.y - rect.top);
2468
+ }
2469
+ /**
2470
+ * Get the effective scale from canvas pixels to graph units,
2471
+ * accounting for preserveAspectRatio="xMidYMid meet" which uses
2472
+ * the smaller scale (to fit) and centers the content.
2473
+ */
2474
+ getEffectiveScale() {
2475
+ const vb = this.currentViewBox();
2476
+ const rect = this.container.getBoundingClientRect();
2477
+ const scaleX = vb.w / rect.width;
2478
+ const scaleY = vb.h / rect.height;
2479
+ const scale = Math.max(scaleX, scaleY);
2480
+ const actualW = rect.width * scale;
2481
+ const actualH = rect.height * scale;
2482
+ const offsetX = (actualW - vb.w) / 2;
2483
+ const offsetY = (actualH - vb.h) / 2;
2484
+ return { scale, offsetX, offsetY };
2485
+ }
2486
+ /** Convert canvas coordinates to graph coordinates */
2487
+ canvasToGraph(canvas) {
2488
+ const vb = this.currentViewBox();
2489
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
2490
+ return graphPos(
2491
+ vb.x - offsetX + canvas.x * scale,
2492
+ vb.y - offsetY + canvas.y * scale
2493
+ );
2494
+ }
2495
+ /** Get current viewBox as an object */
2496
+ currentViewBox() {
2497
+ const p = this.padding;
2498
+ const t = this.transform;
2499
+ const baseX = this.bounds.min.x - p;
2500
+ const baseY = this.bounds.min.y - p;
2501
+ const baseW = this.bounds.max.x - this.bounds.min.x + p * 2;
2502
+ const baseH = this.bounds.max.y - this.bounds.min.y + p * 2;
2503
+ const cx = baseX + baseW / 2;
2504
+ const cy = baseY + baseH / 2;
2505
+ const w = baseW / t.scale;
2506
+ const h = baseH / t.scale;
2507
+ const x = cx - w / 2 - t.x;
2508
+ const y = cy - h / 2 - t.y;
2509
+ return { x, y, w, h };
2510
+ }
2511
+ onWheel(e) {
2512
+ e.preventDefault();
2513
+ const zoomFactor = 1.1;
2514
+ const delta = e.deltaY > 0 ? 1 / zoomFactor : zoomFactor;
2515
+ const screenCursor = screenPos(e.clientX, e.clientY);
2516
+ const canvasCursor = this.screenToCanvas(screenCursor);
2517
+ const graphCursor = this.canvasToGraph(canvasCursor);
2518
+ const oldScale = this.transform.scale;
2519
+ const newScale = Math.max(0.1, Math.min(10, oldScale * delta));
2520
+ this.transform.scale = newScale;
2521
+ const newGraphCursor = this.canvasToGraph(canvasCursor);
2522
+ this.transform.x += newGraphCursor.x - graphCursor.x;
2523
+ this.transform.y += newGraphCursor.y - graphCursor.y;
2524
+ this.applyTransform();
2525
+ }
2526
+ onMouseDown(e) {
2527
+ if (e.button !== 0) return;
2528
+ if (e.target.closest(".g3p-zoom-controls")) return;
2529
+ this.isPanning = true;
2530
+ this.panStart = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2531
+ this.transformStart = { ...this.transform };
2532
+ const { scale } = this.getEffectiveScale();
2533
+ this.panScale = { x: scale, y: scale };
2534
+ this.container.style.cursor = "grabbing";
2535
+ e.preventDefault();
2536
+ }
2537
+ onMouseMove(e) {
2538
+ if (!this.isPanning || !this.panStart || !this.transformStart || !this.panScale) return;
2539
+ const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2540
+ const dx = current.x - this.panStart.x;
2541
+ const dy = current.y - this.panStart.y;
2542
+ this.transform.x = this.transformStart.x + dx * this.panScale.x;
2543
+ this.transform.y = this.transformStart.y + dy * this.panScale.y;
2544
+ this.applyTransform();
2545
+ }
2546
+ onMouseUp(e) {
2547
+ if (!this.isPanning) return;
2548
+ this.isPanning = false;
2549
+ this.panStart = null;
2550
+ this.transformStart = null;
2551
+ this.panScale = null;
2552
+ this.container.style.cursor = "";
2553
+ }
2554
+ applyTransform() {
2555
+ const vb = this.currentViewBox();
2556
+ this.root.setAttribute("viewBox", `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);
2557
+ this.updateZoomLevel();
2558
+ }
2559
+ createZoomControls() {
2560
+ const c = styler("zoom", zoomStyles, this.classPrefix);
2561
+ this.zoomControls = /* @__PURE__ */ jsxs3("div", { className: c("controls"), children: [
2562
+ /* @__PURE__ */ jsx4("button", { className: c("btn"), onClick: () => this.zoomIn(), children: "+" }),
2563
+ /* @__PURE__ */ jsx4("div", { className: c("level"), id: "g3p-zoom-level", children: "100%" }),
2564
+ /* @__PURE__ */ jsx4("button", { className: c("btn"), onClick: () => this.zoomOut(), children: "\u2212" }),
2565
+ /* @__PURE__ */ jsx4("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
2566
+ ] });
2567
+ this.container.appendChild(this.zoomControls);
2568
+ }
2569
+ updateZoomLevel() {
2570
+ const level = this.container.querySelector("#g3p-zoom-level");
2571
+ if (level) {
2572
+ level.textContent = `${Math.round(this.transform.scale * 100)}%`;
2573
+ }
2574
+ }
2575
+ zoomIn() {
2576
+ this.transform.scale = Math.min(10, this.transform.scale * 1.2);
2577
+ this.applyTransform();
2578
+ }
2579
+ zoomOut() {
2580
+ this.transform.scale = Math.max(0.1, this.transform.scale / 1.2);
2581
+ this.applyTransform();
2582
+ }
2583
+ zoomReset() {
2584
+ this.transform = { x: 0, y: 0, scale: 1 };
2585
+ this.applyTransform();
2586
+ }
2185
2587
  };
2186
2588
 
2187
- // src/index.ts
2188
- import { Map as IMap2 } from "immutable";
2189
- var identity = (x) => x;
2190
- var defaultOptions2 = () => ({
2191
- ...defaultOptions,
2192
- nodeProps: identity,
2193
- edgeProps: identity,
2194
- portProps: identity,
2195
- renderNode,
2196
- nodeStyle: (() => ({})),
2197
- edgeStyle: (() => ({})),
2198
- portStyle: "outside",
2199
- width: "100%",
2200
- height: "100%",
2201
- classPrefix: "g3p"
2202
- });
2589
+ // src/canvas/render-node.tsx
2590
+ import { jsx as jsx5, jsxs as jsxs4 } from "jsx-dom/jsx-runtime";
2591
+ function renderNode(node) {
2592
+ if (typeof node == "string") node = { id: node };
2593
+ const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
2594
+ const detail = node?.detail ?? node?.description ?? node?.subtitle;
2595
+ return /* @__PURE__ */ jsxs4("div", { className: "g3p-node-default", children: [
2596
+ /* @__PURE__ */ jsx5("div", { className: "g3p-node-title", children: title }),
2597
+ detail && /* @__PURE__ */ jsx5("div", { className: "g3p-node-detail", children: detail })
2598
+ ] });
2599
+ }
2600
+
2601
+ // src/api/defaults.ts
2602
+ function applyDefaults(options) {
2603
+ const { graph: graph2, canvas, props } = defaults();
2604
+ return {
2605
+ graph: { ...graph2, ...options?.graph },
2606
+ canvas: { ...canvas, ...options?.canvas },
2607
+ props: { ...props, ...options?.props }
2608
+ };
2609
+ }
2610
+ function defaults() {
2611
+ return {
2612
+ graph: {
2613
+ mergeOrder: ["target", "source"],
2614
+ nodeMargin: 15,
2615
+ dummyNodeSize: 15,
2616
+ nodeAlign: "natural",
2617
+ edgeSpacing: 10,
2618
+ turnRadius: 10,
2619
+ orientation: "TB",
2620
+ layerMargin: 5,
2621
+ alignIterations: 5,
2622
+ alignThreshold: 10,
2623
+ separateTrackSets: true,
2624
+ markerSize: 10,
2625
+ layoutSteps: null
2626
+ },
2627
+ canvas: {
2628
+ renderNode,
2629
+ classPrefix: "g3p",
2630
+ width: "100%",
2631
+ height: "100%",
2632
+ padding: 20,
2633
+ editable: false,
2634
+ panZoom: true,
2635
+ markerSize: 10,
2636
+ colorMode: "system",
2637
+ theme: {},
2638
+ nodeTypes: {},
2639
+ edgeTypes: {}
2640
+ },
2641
+ props: {}
2642
+ };
2643
+ }
2644
+
2645
+ // src/api/updater.ts
2646
+ var Updater = class _Updater {
2647
+ update;
2648
+ constructor() {
2649
+ this.update = {
2650
+ addNodes: [],
2651
+ removeNodes: [],
2652
+ updateNodes: [],
2653
+ addEdges: [],
2654
+ removeEdges: [],
2655
+ updateEdges: []
2656
+ };
2657
+ }
2658
+ describe(desc) {
2659
+ this.update.description = desc;
2660
+ return this;
2661
+ }
2662
+ addNode(node) {
2663
+ this.update.addNodes.push(node);
2664
+ return this;
2665
+ }
2666
+ deleteNode(node) {
2667
+ this.update.removeNodes.push(node);
2668
+ return this;
2669
+ }
2670
+ updateNode(node) {
2671
+ this.update.updateNodes.push(node);
2672
+ return this;
2673
+ }
2674
+ addEdge(edge) {
2675
+ this.update.addEdges.push(edge);
2676
+ return this;
2677
+ }
2678
+ deleteEdge(edge) {
2679
+ this.update.removeEdges.push(edge);
2680
+ return this;
2681
+ }
2682
+ updateEdge(edge) {
2683
+ this.update.updateEdges.push(edge);
2684
+ return this;
2685
+ }
2686
+ static add(nodes, edges) {
2687
+ const updater = new _Updater();
2688
+ updater.update.addNodes = nodes;
2689
+ updater.update.addEdges = edges;
2690
+ return updater;
2691
+ }
2692
+ };
2693
+
2694
+ // src/api/api.ts
2695
+ var log11 = logger("api");
2203
2696
  var API = class {
2204
2697
  state;
2205
2698
  seq;
2206
2699
  index;
2207
2700
  canvas;
2208
- _options;
2209
2701
  options;
2210
- constructor(options) {
2211
- this._options = {
2212
- ...defaultOptions2(),
2213
- ...options
2214
- };
2215
- this.options = new Proxy(this._options, {
2216
- set: (target, prop, value) => {
2217
- target[prop] = value;
2218
- this._onOptionChange(prop);
2219
- return true;
2220
- }
2221
- });
2222
- this.state = {
2223
- nodes: IMap2(),
2224
- edges: IMap2(),
2225
- ports: IMap2(),
2226
- segs: IMap2()
2227
- };
2228
- let graph2 = new Graph({ options: this._options });
2229
- this.state.graph = graph2;
2702
+ history;
2703
+ nodeIds;
2704
+ edgeIds;
2705
+ nodeVersions;
2706
+ nextNodeId;
2707
+ nextEdgeId;
2708
+ root;
2709
+ constructor(args) {
2710
+ this.root = args.root;
2711
+ this.options = applyDefaults(args.options);
2712
+ let graph2 = new Graph({ options: this.options.graph });
2713
+ this.state = { graph: graph2, update: null };
2230
2714
  this.seq = [this.state];
2231
2715
  this.index = 0;
2232
- this.canvas = new Canvas(this._options);
2233
- this.canvas.render();
2716
+ this.nodeIds = /* @__PURE__ */ new Map();
2717
+ this.edgeIds = /* @__PURE__ */ new Map();
2718
+ this.nodeVersions = /* @__PURE__ */ new Map();
2719
+ this.nextNodeId = 1;
2720
+ this.nextEdgeId = 1;
2721
+ this.canvas = new Canvas({
2722
+ ...this.options.canvas,
2723
+ dummyNodeSize: this.options.graph.dummyNodeSize,
2724
+ orientation: this.options.graph.orientation
2725
+ });
2726
+ if (args.history) {
2727
+ this.history = args.history;
2728
+ } else if (args.nodes) {
2729
+ this.history = [Updater.add(args.nodes, args.edges || []).update];
2730
+ } else {
2731
+ this.history = [];
2732
+ }
2234
2733
  }
2235
- render() {
2236
- return this.canvas.container;
2734
+ async init() {
2735
+ const root = document.getElementById(this.root);
2736
+ if (!root) throw new Error("root element not found");
2737
+ root.appendChild(this.canvas.container);
2738
+ for (const update of this.history)
2739
+ await this.applyUpdate(update);
2237
2740
  }
2238
2741
  nav(nav) {
2239
2742
  let newIndex;
@@ -2258,36 +2761,27 @@ var API = class {
2258
2761
  this.state = this.seq[this.index];
2259
2762
  }
2260
2763
  applyDiff(oldIndex, newIndex) {
2261
- const oldState = this.seq[oldIndex];
2262
- const newState = this.seq[newIndex];
2263
- this.canvas.update(() => {
2264
- for (const oldNode of oldState.nodes.values()) {
2265
- const newNode = newState.nodes.get(oldNode.id);
2266
- if (!newNode) {
2267
- this.canvas.deleteNode(oldNode);
2268
- } else if (oldNode.data !== newNode.data) {
2269
- this.canvas.deleteNode(oldNode);
2270
- this.canvas.addNode(newNode);
2271
- } else if (oldNode.pos.x !== newNode.pos.x || oldNode.pos.y !== newNode.pos.y) {
2272
- this.canvas.updateNode(newNode);
2273
- }
2274
- }
2275
- for (const newNode of newState.nodes.values()) {
2276
- if (!oldState.nodes.has(newNode.id))
2277
- this.canvas.addNode(newNode);
2278
- }
2279
- for (const oldSeg of oldState.segs.values()) {
2280
- const newSeg = newState.segs.get(oldSeg.segId);
2281
- if (!newSeg)
2282
- this.canvas.deleteSeg(oldSeg);
2283
- else if (oldSeg.svg != newSeg.svg)
2284
- this.canvas.updateSeg(newSeg);
2285
- }
2286
- for (const newSeg of newState.segs.values()) {
2287
- if (!oldState.segs.has(newSeg.segId))
2288
- this.canvas.addSeg(newSeg);
2289
- }
2290
- });
2764
+ const oldGraph = this.seq[oldIndex].graph;
2765
+ const newGraph = this.seq[newIndex].graph;
2766
+ for (const oldNode of oldGraph.nodes.values()) {
2767
+ const newNode = newGraph.nodes.get(oldNode.id);
2768
+ if (!newNode) this.canvas.deleteNode(oldNode);
2769
+ }
2770
+ for (const newNode of newGraph.nodes.values()) {
2771
+ if (!oldGraph.nodes.has(newNode.id)) this.canvas.addNode(newNode);
2772
+ }
2773
+ for (const oldSeg of oldGraph.segs.values()) {
2774
+ const newSeg = newGraph.segs.get(oldSeg.id);
2775
+ if (!newSeg)
2776
+ this.canvas.deleteSeg(oldSeg);
2777
+ else if (oldSeg.svg != newSeg.svg)
2778
+ this.canvas.updateSeg(newSeg);
2779
+ }
2780
+ for (const newSeg of newGraph.segs.values()) {
2781
+ if (!oldGraph.segs.has(newSeg.id))
2782
+ this.canvas.addSeg(newSeg, newGraph);
2783
+ }
2784
+ this.canvas.update();
2291
2785
  }
2292
2786
  async addNode(node) {
2293
2787
  await this.update((update) => update.addNode(node));
@@ -2304,190 +2798,175 @@ var API = class {
2304
2798
  async deleteEdge(edge) {
2305
2799
  await this.update((update) => update.deleteEdge(edge));
2306
2800
  }
2307
- async update(callback) {
2308
- const update = new Update();
2309
- callback(update);
2310
- await this.measureNodes(update);
2311
- const newGraph = this.state.graph.withMutations((mut) => {
2312
- this.state = {
2313
- nodes: this.state.nodes.asMutable(),
2314
- edges: this.state.edges.asMutable(),
2315
- ports: this.state.ports.asMutable(),
2316
- segs: this.state.segs.asMutable()
2317
- };
2318
- for (const node of update.updatedNodes)
2319
- this._updateNode(node, mut);
2320
- for (const edge of update.updatedEdges)
2321
- this._updateEdge(edge, mut);
2322
- for (const node of update.addedNodes)
2323
- this._addNode(node, mut);
2324
- for (const node of update.removedNodes)
2801
+ async applyUpdate(update) {
2802
+ log11.info("applyUpdate", update);
2803
+ const nodes = await this.measureNodes(update);
2804
+ const graph2 = this.state.graph.withMutations((mut) => {
2805
+ for (const edge of update.removeEdges ?? [])
2806
+ this._removeEdge(edge, mut);
2807
+ for (const node of update.removeNodes ?? [])
2325
2808
  this._removeNode(node, mut);
2326
- for (const edge of update.addedEdges)
2809
+ for (const node of update.addNodes ?? [])
2810
+ this._addNode(nodes.get(node), mut);
2811
+ for (const node of update.updateNodes ?? [])
2812
+ this._updateNode(nodes.get(node), mut);
2813
+ for (const edge of update.addEdges ?? [])
2327
2814
  this._addEdge(edge, mut);
2328
- for (const edge of update.removedEdges)
2329
- this._removeEdge(edge, mut);
2815
+ for (const edge of update.updateEdges ?? [])
2816
+ this._updateEdge(edge, mut);
2330
2817
  });
2331
- console.log("new graph:", newGraph);
2332
- for (const nodeId of newGraph.dirtyNodes.values()) {
2333
- const node = newGraph.getNode(nodeId);
2334
- console.log(`got pos of node ${nodeId}:`, node.pos);
2335
- if (node.isDummy) {
2336
- this.state.nodes.set(nodeId, { id: nodeId, pos: node.pos, isDummy: true });
2337
- } else {
2338
- const myNode = this.state.nodes.get(nodeId);
2339
- this.state.nodes.set(nodeId, { ...myNode, pos: node.pos });
2340
- }
2341
- }
2342
- for (const nodeId of newGraph.delNodes)
2343
- this.state.nodes.delete(nodeId);
2344
- for (const segId of newGraph.delSegs)
2345
- this.state.segs.delete(segId);
2346
- for (const segId of newGraph.dirtySegs) {
2347
- const seg = newGraph.getSeg(segId);
2348
- const edge = this.state.edges.get(seg.edgeIds.values().next().value);
2349
- const target = seg.targetNode(newGraph);
2350
- this.state.segs.set(seg.id, {
2351
- segId: seg.id,
2352
- edgeId: edge.id,
2353
- svg: seg.svg,
2354
- attrs: edge.attrs,
2355
- targetDummy: target.isDummy,
2356
- edgeData: edge.data
2357
- });
2358
- }
2359
- this.state = {
2360
- nodes: this.state.nodes.asImmutable(),
2361
- edges: this.state.edges.asImmutable(),
2362
- ports: this.state.ports.asImmutable(),
2363
- segs: this.state.segs.asImmutable(),
2364
- graph: newGraph,
2365
- update
2366
- };
2818
+ this.state = { graph: graph2, update };
2819
+ this.setNodePositions();
2367
2820
  this.seq.splice(this.index + 1);
2368
2821
  this.seq.push(this.state);
2369
2822
  this.nav("last");
2370
2823
  }
2371
- async measureNodes(update) {
2372
- const nodes = update.updatedNodes.concat(update.addedNodes);
2373
- await this.canvas.measure(nodes);
2374
- }
2375
- getDims(node) {
2376
- return this.canvas.getDims(node);
2377
- }
2378
- _updateNode(node, mut) {
2379
- const props = this._options.nodeProps(node);
2380
- const oldData = this.state.nodes.get(props.id);
2381
- if (oldData === void 0)
2382
- throw new Error(`updating node ${props.id} which does not exist`);
2383
- const attrs = this._options.nodeStyle(node);
2384
- mut.updateNode({ id: props.id, dims: this.getDims(node) });
2385
- const data = { id: props.id, attrs, orig: node, node: props };
2386
- this.state.nodes.set(props.id, data);
2387
- }
2388
- _updateEdge(edge, mut) {
2389
- const props = this._options.edgeProps(edge);
2390
- const id = Edge.id(props), str = Edge.str(props);
2391
- const oldData = this.state.edges.get(id);
2392
- if (oldData === void 0)
2393
- throw new Error(`updating edge ${str} which does not exist`);
2394
- const attrs = props.type ? this._options.edgeStyle(props.type) : void 0;
2395
- const data = { id, attrs, data: edge, edge: props };
2396
- this.state.edges.set(id, data);
2397
- if (props.type !== oldData.edge.type) {
2398
- mut.removeEdge(oldData.edge);
2399
- mut.addEdge(props);
2400
- } else {
2824
+ setNodePositions() {
2825
+ const { graph: graph2 } = this.state;
2826
+ for (const nodeId of graph2.dirtyNodes) {
2827
+ const node = graph2.getNode(nodeId);
2828
+ if (!node.isDummy)
2829
+ this.canvas.getNode(node.key).setPos(node.pos);
2401
2830
  }
2402
2831
  }
2403
- _addNode(node, mut) {
2404
- const props = this._options.nodeProps(node);
2405
- if (this.state.nodes.has(props.id))
2406
- throw new Error(`node with id ${props.id} already exists`);
2407
- props.ports = { in: [], out: [], ...props.ports };
2408
- const attrs = this._options.nodeStyle(node);
2409
- const data = { id: props.id, attrs, data: node, node: props };
2410
- this.state.nodes.set(props.id, data);
2411
- console.log("adding node:", { ...props, dims: this.getDims(node) });
2412
- mut.addNode({ ...props, dims: this.getDims(node) });
2832
+ async update(callback) {
2833
+ const updater = new Updater();
2834
+ callback(updater);
2835
+ await this.applyUpdate(updater.update);
2413
2836
  }
2414
- _removeNode(node, mut) {
2415
- const props = this._options.nodeProps(node);
2416
- if (!this.state.nodes.has(props.id))
2417
- throw new Error(`removing node ${props.id} which does not exist`);
2418
- this.state.nodes.delete(props.id);
2419
- mut.removeNode(props);
2837
+ async measureNodes(update) {
2838
+ const data = [];
2839
+ for (const set of [update.updateNodes, update.addNodes])
2840
+ for (const node of set ?? [])
2841
+ data.push(this.parseNode(node, true));
2842
+ return await this.canvas.measureNodes(data);
2843
+ }
2844
+ parseNode(data, bumpVersion = false) {
2845
+ const get = this.options.props.node;
2846
+ let props;
2847
+ if (get) props = get(data);
2848
+ else if (!data) throw new Error(`invalid node ${data}`);
2849
+ else if (typeof data == "string") props = { id: data };
2850
+ else if (typeof data == "object") props = data;
2851
+ else throw new Error(`invalid node ${data}`);
2852
+ let { id, title, text, type, render } = props;
2853
+ id ??= this.getNodeId(data);
2854
+ const ports = this.parsePorts(props.ports);
2855
+ let version = this.nodeVersions.get(data);
2856
+ if (!version) version = 1;
2857
+ else if (bumpVersion) version++;
2858
+ this.nodeVersions.set(data, version);
2859
+ return { id, data, ports, title, text, type, render, version };
2860
+ }
2861
+ parseEdge(data) {
2862
+ const get = this.options.props.edge;
2863
+ let props;
2864
+ if (get) props = get(data);
2865
+ else if (!data) throw new Error(`invalid edge ${data}`);
2866
+ else if (typeof data == "string") props = this.parseStringEdge(data);
2867
+ else if (typeof data == "object") props = data;
2868
+ else throw new Error(`invalid edge ${data}`);
2869
+ let { id, source, target, type } = props;
2870
+ id ??= this.getEdgeId(data);
2871
+ source = this.parseEdgeEnd(source);
2872
+ target = this.parseEdgeEnd(target);
2873
+ const edge = { id, source, target, type, data };
2874
+ return edge;
2420
2875
  }
2421
- _addEdge(edge, mut) {
2422
- const props = this._options.edgeProps(edge);
2423
- const id = Edge.id(props), str = Edge.str(props);
2424
- if (this.state.edges.has(id))
2425
- throw new Error(`edge ${str} already exists`);
2426
- const attrs = props.type ? this._options.edgeStyle(props.type) : void 0;
2427
- const data = { id, attrs, data: edge, edge: props };
2428
- this.state.edges.set(id, data);
2429
- mut.addEdge(props);
2876
+ parseEdgeEnd(end) {
2877
+ if (!end) throw new Error(`edge has an undefined source or target`);
2878
+ if (typeof end == "string") return { id: end };
2879
+ if (typeof end == "object") {
2880
+ const keys = Object.keys(end);
2881
+ const pidx = keys.indexOf("port");
2882
+ if (pidx != -1) {
2883
+ if (typeof end.port != "string") return end;
2884
+ keys.splice(pidx, 1);
2885
+ }
2886
+ if (keys.length != 1) return end;
2887
+ if (keys[0] == "id") return end;
2888
+ if (keys[0] != "node") return end;
2889
+ const id = this.nodeIds.get(end.node);
2890
+ if (!id) throw new Error(`edge end ${end} references unknown node ${end.node}`);
2891
+ return { id, port: end.port };
2892
+ }
2893
+ throw new Error(`invalid edge end ${end}`);
2430
2894
  }
2431
- _removeEdge(edge, mut) {
2432
- const props = this._options.edgeProps(edge);
2433
- const id = Edge.id(props), str = Edge.str(props);
2434
- if (!this.state.edges.has(id))
2435
- throw new Error(`removing edge ${str} which does not exist`);
2436
- this.state.edges.delete(id);
2437
- mut.removeEdge(props);
2895
+ parseStringEdge(str) {
2896
+ const [source, target] = str.split(/\s*(?::|-+>?)\s*/);
2897
+ return { source, target };
2438
2898
  }
2439
- _onOptionChange(prop) {
2899
+ parsePorts(ports) {
2900
+ const fixed = { in: null, out: null };
2901
+ for (const key of ["in", "out"]) {
2902
+ if (ports?.[key] && ports[key].length > 0)
2903
+ fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
2904
+ }
2905
+ return fixed;
2440
2906
  }
2441
- };
2442
- var Update = class {
2443
- addedNodes;
2444
- removedNodes;
2445
- updatedNodes;
2446
- addedEdges;
2447
- removedEdges;
2448
- updatedEdges;
2449
- desc;
2450
- constructor() {
2451
- this.addedNodes = [];
2452
- this.removedNodes = [];
2453
- this.updatedNodes = [];
2454
- this.addedEdges = [];
2455
- this.removedEdges = [];
2456
- this.updatedEdges = [];
2907
+ getNodeId(node) {
2908
+ let id = this.nodeIds.get(node);
2909
+ if (!id) {
2910
+ id = `n${this.nextNodeId++}`;
2911
+ this.nodeIds.set(node, id);
2912
+ }
2913
+ return id;
2457
2914
  }
2458
- describe(desc) {
2459
- this.desc = desc;
2915
+ getEdgeId(edge) {
2916
+ let id = this.edgeIds.get(edge);
2917
+ if (!id) {
2918
+ id = `e${this.nextEdgeId++}`;
2919
+ this.edgeIds.set(edge, id);
2920
+ }
2921
+ return id;
2460
2922
  }
2461
- addNode(node) {
2462
- this.addedNodes.push(node);
2923
+ _addNode(node, mut) {
2924
+ const { data, id: newId } = node.data;
2925
+ const oldId = this.nodeIds.get(data);
2926
+ console.log("addNode", node, oldId, newId);
2927
+ if (oldId && oldId != newId)
2928
+ throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
2929
+ this.nodeIds.set(data, newId);
2930
+ mut.addNode(node.data);
2463
2931
  }
2464
- deleteNode(node) {
2465
- this.removedNodes.push(node);
2932
+ _removeNode(node, mut) {
2933
+ const id = this.nodeIds.get(node);
2934
+ if (!id) throw new Error(`removing node ${node} which does not exist`);
2935
+ mut.removeNode({ id });
2466
2936
  }
2467
- updateNode(node) {
2468
- this.updatedNodes.push(node);
2937
+ _updateNode(node, mut) {
2938
+ const { data, id: newId } = node.data;
2939
+ const oldId = this.nodeIds.get(data);
2940
+ if (!oldId) throw new Error(`updating unknown node ${node}`);
2941
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
2942
+ mut.updateNode(node.data);
2469
2943
  }
2470
- addEdge(edge) {
2471
- this.addedEdges.push(edge);
2944
+ _addEdge(edge, mut) {
2945
+ const data = this.parseEdge(edge);
2946
+ const id = this.edgeIds.get(edge);
2947
+ if (id && id != data.id)
2948
+ throw new Error(`edge id changed from ${id} to ${data.id}`);
2949
+ this.edgeIds.set(edge, data.id);
2950
+ mut.addEdge(data);
2472
2951
  }
2473
- deleteEdge(edge) {
2474
- this.removedEdges.push(edge);
2952
+ _removeEdge(edge, mut) {
2953
+ const id = this.edgeIds.get(edge);
2954
+ if (!id) throw new Error(`removing edge ${edge} which does not exist`);
2955
+ mut.removeEdge(this.parseEdge(edge));
2475
2956
  }
2476
- updateEdge(edge) {
2477
- this.updatedEdges.push(edge);
2957
+ _updateEdge(edge, mut) {
2958
+ const id = this.edgeIds.get(edge);
2959
+ if (!id) throw new Error(`updating unknown edge ${edge}`);
2960
+ const data = this.parseEdge(edge);
2961
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
2962
+ mut.updateEdge(data);
2478
2963
  }
2479
2964
  };
2480
- async function graph(args = {}) {
2481
- const { nodes = [], edges = [], ...options } = args;
2482
- const api = new API(options);
2483
- if (nodes.length > 0 || edges.length > 0) {
2484
- await api.update((update) => {
2485
- for (const node of nodes)
2486
- update.addNode(node);
2487
- for (const edge of edges)
2488
- update.addEdge(edge);
2489
- });
2490
- }
2965
+
2966
+ // src/index.ts
2967
+ async function graph(args = { root: "app" }) {
2968
+ const api = new API(args);
2969
+ await api.init();
2491
2970
  return api;
2492
2971
  }
2493
2972
  var index_default = graph;