@3plate/graph-core 0.1.2 → 0.1.5

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