@3plate/graph-core 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,6 +40,70 @@ var import_immutable8 = require("immutable");
40
40
 
41
41
  // src/graph/node.ts
42
42
  var import_immutable = require("immutable");
43
+
44
+ // src/log.ts
45
+ var import_pino = __toESM(require("pino"), 1);
46
+ var resolvedLevel = typeof globalThis !== "undefined" && globalThis.__LOG_LEVEL || typeof process !== "undefined" && process.env?.LOG_LEVEL || "debug";
47
+ var browserOpts = { asObject: true };
48
+ browserOpts.transmit = {
49
+ level: resolvedLevel,
50
+ send: (level, log12) => {
51
+ try {
52
+ const endpoint = typeof globalThis !== "undefined" && globalThis.__LOG_INGEST_URL || typeof process !== "undefined" && process.env?.LOG_INGEST_URL || void 0;
53
+ if (!endpoint || typeof window === "undefined") {
54
+ try {
55
+ console.debug("[graph-core] transmit skipped", { endpoint, hasWindow: typeof window !== "undefined", level });
56
+ } catch {
57
+ }
58
+ return;
59
+ }
60
+ const line = JSON.stringify({ level, ...log12, ts: Date.now() }) + "\n";
61
+ try {
62
+ console.debug("[graph-core] transmit sending", { endpoint, level, bytes: line.length });
63
+ } catch {
64
+ }
65
+ fetch(endpoint, {
66
+ method: "POST",
67
+ mode: "no-cors",
68
+ credentials: "omit",
69
+ body: line,
70
+ keepalive: true
71
+ }).then(() => {
72
+ try {
73
+ console.debug("[graph-core] transmit fetch ok");
74
+ } catch {
75
+ }
76
+ }).catch((err) => {
77
+ try {
78
+ console.debug("[graph-core] transmit fetch error", err?.message || err);
79
+ } catch {
80
+ }
81
+ });
82
+ } catch (e) {
83
+ try {
84
+ console.debug("[graph-core] transmit error", e?.message || e);
85
+ } catch {
86
+ }
87
+ }
88
+ }
89
+ };
90
+ var base = (0, import_pino.default)({
91
+ level: resolvedLevel,
92
+ browser: browserOpts
93
+ });
94
+ function logger(module2) {
95
+ const child = base.child({ module: module2 });
96
+ return {
97
+ error: (msg, ...args) => child.error({ args }, msg),
98
+ warn: (msg, ...args) => child.warn({ args }, msg),
99
+ info: (msg, ...args) => child.info({ args }, msg),
100
+ debug: (msg, ...args) => child.debug({ args }, msg)
101
+ };
102
+ }
103
+ var log = logger("core");
104
+
105
+ // src/graph/node.ts
106
+ var log2 = logger("node");
43
107
  var defNodeData = {
44
108
  id: "",
45
109
  data: void 0,
@@ -48,7 +112,7 @@ var defNodeData = {
48
112
  text: void 0,
49
113
  type: void 0,
50
114
  render: void 0,
51
- ports: { in: null, out: null },
115
+ ports: {},
52
116
  aligned: {},
53
117
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
54
118
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
@@ -82,10 +146,7 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
82
146
  return this.isDummy ? this.id : _Node.key(this);
83
147
  }
84
148
  static key(node) {
85
- return `${node.id}:${node.version}`;
86
- }
87
- static isDummyId(nodeId) {
88
- return nodeId.startsWith(_Node.dummyPrefix);
149
+ return `k:${node.id}:${node.version}`;
89
150
  }
90
151
  static addNormal(g, data) {
91
152
  const layer = g.layerAt(0);
@@ -95,7 +156,9 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
95
156
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
96
157
  aligned: {},
97
158
  edgeIds: [],
98
- layerId: layer.id
159
+ layerId: layer.id,
160
+ lpos: void 0,
161
+ pos: void 0
99
162
  });
100
163
  layer.addNode(g, node.id);
101
164
  g.nodes.set(node.id, node);
@@ -158,6 +221,18 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
158
221
  getLayer(g) {
159
222
  return g.getLayer(this.layerId);
160
223
  }
224
+ margin(g) {
225
+ return this.isDummy ? g.options.edgeSpacing - g.options.dummyNodeSize : g.options.nodeMargin;
226
+ }
227
+ marginWith(g, other) {
228
+ return Math.max(this.margin(g), other.margin(g));
229
+ }
230
+ width(g) {
231
+ return this.dims?.[g.w] ?? 0;
232
+ }
233
+ right(g) {
234
+ return this.lpos + this.width(g);
235
+ }
161
236
  setIndex(g, index) {
162
237
  if (this.index == index) return this;
163
238
  return this.mut(g).set("index", index);
@@ -204,9 +279,22 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
204
279
  return node;
205
280
  }
206
281
  delSelf(g) {
282
+ if (this.aligned.in)
283
+ g.getNode(this.aligned.in).setAligned(g, "out", void 0);
284
+ if (this.aligned.out)
285
+ g.getNode(this.aligned.out).setAligned(g, "in", void 0);
207
286
  this.getLayer(g).delNode(g, this.id);
208
- for (const rel of this.rels(g))
209
- rel.delSelf(g);
287
+ for (const edge of this.rels(g, "edges", "both"))
288
+ edge.delSelf(g);
289
+ const remainingSegIds = [...this.segs.in, ...this.segs.out];
290
+ if (remainingSegIds.length > 0) {
291
+ for (const segId of remainingSegIds) {
292
+ if (g.segs?.has?.(segId)) {
293
+ g.getSeg(segId).delSelf(g);
294
+ } else {
295
+ }
296
+ }
297
+ }
210
298
  g.nodes.delete(this.id);
211
299
  g.dirtyNodes.delete(this.id);
212
300
  g.delNodes.add(this.id);
@@ -299,30 +387,7 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
299
387
 
300
388
  // src/graph/edge.ts
301
389
  var import_immutable2 = require("immutable");
302
-
303
- // src/log.ts
304
- var levels = {
305
- error: 0,
306
- warn: 1,
307
- info: 2,
308
- debug: 3
309
- };
310
- var currentLevel = "debug";
311
- function shouldLog(level) {
312
- return levels[level] <= levels[currentLevel];
313
- }
314
- function logger(module2) {
315
- return {
316
- error: (msg, ...args) => shouldLog("error") && console.error(`[${module2}] ${msg}`, ...args),
317
- warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module2}] ${msg}`, ...args),
318
- info: (msg, ...args) => shouldLog("info") && console.info(`[${module2}] ${msg}`, ...args),
319
- debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module2}] ${msg}`, ...args)
320
- };
321
- }
322
- var log = logger("core");
323
-
324
- // src/graph/edge.ts
325
- var log2 = logger("edge");
390
+ var log3 = logger("edge");
326
391
  var defEdgeData = {
327
392
  id: "",
328
393
  data: null,
@@ -330,7 +395,6 @@ var defEdgeData = {
330
395
  source: { id: "" },
331
396
  target: { id: "" },
332
397
  type: void 0,
333
- style: void 0,
334
398
  mutable: false,
335
399
  segIds: []
336
400
  };
@@ -411,7 +475,7 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
411
475
  source = edge.source.id;
412
476
  if (edge.source?.port)
413
477
  source = `${source}.${edge.source.port}`;
414
- const marker = edge.source?.marker ?? edge.style?.marker?.source;
478
+ const marker = edge.source?.marker;
415
479
  if (marker && marker != "none") source += `[${marker}]`;
416
480
  source += "-";
417
481
  }
@@ -421,7 +485,7 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
421
485
  if (edge.target.port)
422
486
  target = `${target}.${edge.target.port}`;
423
487
  target = "-" + target;
424
- const marker = edge.target?.marker ?? edge.style?.marker?.target ?? "arrow";
488
+ const marker = edge.target?.marker ?? "arrow";
425
489
  if (marker && marker != "none") target += `[${marker}]`;
426
490
  }
427
491
  const type = edge.type || "";
@@ -463,7 +527,6 @@ var defSegData = {
463
527
  source: { id: "" },
464
528
  target: { id: "" },
465
529
  type: void 0,
466
- style: void 0,
467
530
  edgeIds: (0, import_immutable3.Set)(),
468
531
  trackPos: void 0,
469
532
  svg: void 0,
@@ -494,7 +557,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
494
557
  sameEnd(other, side) {
495
558
  const mine = this[side];
496
559
  const yours = other[side];
497
- return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
560
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker && this.type === other.type;
498
561
  }
499
562
  setPos(g, source, target) {
500
563
  return this.mut(g).merge({
@@ -566,7 +629,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
566
629
 
567
630
  // src/graph/layer.ts
568
631
  var import_immutable4 = require("immutable");
569
- var log3 = logger("layer");
632
+ var log4 = logger("layer");
570
633
  var defLayerData = {
571
634
  id: "",
572
635
  index: 0,
@@ -591,7 +654,7 @@ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
591
654
  mutable: false
592
655
  }).asImmutable();
593
656
  }
594
- get size() {
657
+ get nodeCount() {
595
658
  return this.nodeIds.size;
596
659
  }
597
660
  *nodes(g) {
@@ -646,9 +709,8 @@ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
646
709
  reindex(g, nodeId) {
647
710
  if (!this.isSorted) return void 0;
648
711
  const sorted = this.sorted.filter((id) => id != nodeId);
649
- const idx = this.sorted.findIndex((id) => id == nodeId);
650
- for (let i = idx; i < sorted.length; i++)
651
- g.getNode(sorted[i]).setIndex(g, i);
712
+ for (const [i, id] of this.sorted.entries())
713
+ g.getNode(id).setIndex(g, i);
652
714
  return sorted;
653
715
  }
654
716
  delNode(g, nodeId) {
@@ -820,12 +882,12 @@ var Cycles = class _Cycles {
820
882
 
821
883
  // src/graph/services/dummy.ts
822
884
  var import_immutable5 = require("immutable");
823
- var log4 = logger("dummy");
885
+ var log5 = logger("dummy");
824
886
  var Dummy = class _Dummy {
825
887
  static updateDummies(g) {
826
888
  for (const edgeId of g.dirtyEdges) {
827
889
  const edge = g.getEdge(edgeId);
828
- const { type, style } = edge;
890
+ const { type } = edge;
829
891
  const sourceLayer = edge.sourceNode(g).layerIndex(g);
830
892
  const targetLayer = edge.targetNode(g).layerIndex(g);
831
893
  let segIndex = 0;
@@ -849,7 +911,7 @@ var Dummy = class _Dummy {
849
911
  });
850
912
  target = { id: dummy.id };
851
913
  }
852
- seg = Seg.add(g, { source, target, type, style, edgeIds: (0, import_immutable5.Set)([edgeId]) });
914
+ seg = Seg.add(g, { source, target, type, edgeIds: (0, import_immutable5.Set)([edgeId]) });
853
915
  segs.splice(segIndex, 0, seg.id);
854
916
  changed = true;
855
917
  } else if (segLayer < layerIndex || seg.source.id != source.id || seg.source.port != source.port || layerIndex == targetLayer && (seg.target.id != edge.target.id || seg.target.port != edge.target.port)) {
@@ -888,9 +950,8 @@ var Dummy = class _Dummy {
888
950
  let layer = g.getLayer(layerId);
889
951
  const groups = /* @__PURE__ */ new Map();
890
952
  for (const nodeId of layer.nodeIds) {
891
- if (!Node.isDummyId(nodeId)) continue;
892
953
  const node = g.getNode(nodeId);
893
- if (node.isMerged) continue;
954
+ if (!node.isDummy || node.isMerged) continue;
894
955
  const edge = g.getEdge(node.edgeIds[0]);
895
956
  const key = Edge.key(edge, "k:", side);
896
957
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
@@ -898,7 +959,7 @@ var Dummy = class _Dummy {
898
959
  }
899
960
  for (const [key, group] of groups) {
900
961
  if (group.size == 1) continue;
901
- const edgeIds = [...group].map((node) => node.edgeId);
962
+ const edgeIds = [...group].map((node) => node.edgeIds[0]);
902
963
  const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
903
964
  let seg;
904
965
  for (const old of group) {
@@ -908,8 +969,9 @@ var Dummy = class _Dummy {
908
969
  const example = g.getSeg(segId);
909
970
  seg = Seg.add(g, {
910
971
  ...example,
911
- edgeIds: (0, import_immutable5.Set)([old.edgeId]),
912
- [altSide]: { ...example[altSide], id: dummy.id }
972
+ edgeIds: (0, import_immutable5.Set)(edgeIds),
973
+ [side]: { ...example[side] },
974
+ [altSide]: { ...example[altSide], id: dummy.id, port: void 0 }
913
975
  });
914
976
  }
915
977
  edge = edge.replaceSegId(g, segId, seg.id);
@@ -921,12 +983,15 @@ var Dummy = class _Dummy {
921
983
  const example = g.getSeg(segId);
922
984
  const seg2 = Seg.add(g, {
923
985
  ...example,
924
- edgeIds: (0, import_immutable5.Set)([old.edgeId]),
925
- [side]: { ...example[side], id: dummy.id }
986
+ edgeIds: (0, import_immutable5.Set)([old.edgeIds[0]]),
987
+ [altSide]: { ...example[altSide] },
988
+ [side]: { ...example[side], id: dummy.id, port: void 0 }
926
989
  });
927
990
  edge = edge.replaceSegId(g, segId, seg2.id);
928
991
  }
929
992
  }
993
+ for (const old of group)
994
+ old.delSelf(g);
930
995
  }
931
996
  }
932
997
  }
@@ -934,7 +999,7 @@ var Dummy = class _Dummy {
934
999
 
935
1000
  // src/graph/services/layers.ts
936
1001
  var import_immutable6 = require("immutable");
937
- var log5 = logger("layers");
1002
+ var log6 = logger("layers");
938
1003
  var Layers = class {
939
1004
  static updateLayers(g) {
940
1005
  const stack = [...g.dirtyNodes].map((id) => g.getNode(id)).filter((node) => !node.isDummy).sort((a, b) => b.layerIndex(g) - a.layerIndex(g)).map((node) => node.id);
@@ -997,7 +1062,7 @@ var Layers = class {
997
1062
 
998
1063
  // src/graph/services/layout.ts
999
1064
  var import_immutable7 = require("immutable");
1000
- var log6 = logger("layout");
1065
+ var log7 = logger("layout");
1001
1066
  var Layout = class _Layout {
1002
1067
  static parentIndex(g, node) {
1003
1068
  const parents = (0, import_immutable7.Seq)([...node.adjs(g, "segs", "in")]);
@@ -1014,8 +1079,8 @@ var Layout = class _Layout {
1014
1079
  if (a.isDummy && !b.isDummy) return -1;
1015
1080
  if (!a.isDummy && b.isDummy) return 1;
1016
1081
  if (!a.isDummy) return a.id.localeCompare(b.id);
1017
- const minA = a.edgeId ?? (0, import_immutable7.Seq)(a.edgeIds).min();
1018
- const minB = b.edgeId ?? (0, import_immutable7.Seq)(b.edgeIds).min();
1082
+ const minA = (0, import_immutable7.Seq)(a.edgeIds).min();
1083
+ const minB = (0, import_immutable7.Seq)(b.edgeIds).min();
1019
1084
  return minA.localeCompare(minB);
1020
1085
  }
1021
1086
  static positionNodes(g) {
@@ -1041,7 +1106,12 @@ var Layout = class _Layout {
1041
1106
  let node = g.getNode(sorted[i]);
1042
1107
  node = node.setIndex(g, i).setLayerPos(g, lpos);
1043
1108
  const size = node.dims?.[g.w] ?? 0;
1044
- lpos += size + g.options.nodeMargin;
1109
+ let margin = node.margin(g);
1110
+ if (i + 1 < sorted.length) {
1111
+ const next = g.getNode(sorted[i + 1]);
1112
+ margin = node.marginWith(g, next);
1113
+ }
1114
+ lpos += size + margin;
1045
1115
  }
1046
1116
  }
1047
1117
  }
@@ -1073,7 +1143,7 @@ var Layout = class _Layout {
1073
1143
  let iterations = 0;
1074
1144
  while (true) {
1075
1145
  if (++iterations > 10) {
1076
- log6.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1146
+ log7.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1077
1147
  break;
1078
1148
  }
1079
1149
  let changed = false;
@@ -1133,7 +1203,7 @@ var Layout = class _Layout {
1133
1203
  static anchorPos(g, seg, side) {
1134
1204
  const nodeId = seg[side].id;
1135
1205
  const node = g.getNode(nodeId);
1136
- let p = { x: 0, y: 0, [g.x]: node.lpos, ...node.pos || {} };
1206
+ let p = { [g.x]: node.lpos, [g.y]: node.pos?.[g.y] ?? 0 };
1137
1207
  let w = node.dims?.[g.w] ?? 0;
1138
1208
  let h = node.dims?.[g.h] ?? 0;
1139
1209
  if (node.isDummy)
@@ -1179,22 +1249,18 @@ var Layout = class _Layout {
1179
1249
  }
1180
1250
  static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1181
1251
  const node = g.getNode(nodeId);
1182
- log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
1183
1252
  if (!conservative)
1184
1253
  _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1185
- const space = g.options.nodeMargin;
1186
- const nodeWidth = node.dims?.[g.w] ?? 0;
1187
- const aMin = lpos - space, aMax = lpos + nodeWidth + space;
1254
+ const nodeRight = lpos + node.width(g);
1188
1255
  repeat:
1189
1256
  for (const otherId of node.getLayer(g).nodeIds) {
1190
1257
  if (otherId == nodeId) continue;
1191
1258
  const other = g.getNode(otherId);
1192
- const opos = other.lpos;
1193
- const otherWidth = other.dims?.[g.w] ?? 0;
1194
- const bMin = opos, bMax = opos + otherWidth;
1195
- if (aMin < bMax && bMin < aMax) {
1259
+ const margin = node.marginWith(g, other);
1260
+ const gap = lpos < other.lpos ? other.lpos - nodeRight : lpos - other.right(g);
1261
+ if (gap < margin) {
1196
1262
  if (conservative) return false;
1197
- const safePos = reverseMove ? aMin - otherWidth : aMax;
1263
+ const safePos = reverseMove ? lpos - other.width(g) - margin : nodeRight + margin;
1198
1264
  _Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
1199
1265
  continue repeat;
1200
1266
  }
@@ -1243,11 +1309,12 @@ var Layout = class _Layout {
1243
1309
  for (const layerId of g.layerList) {
1244
1310
  const layer = g.getLayer(layerId);
1245
1311
  if (layer.sorted.length < 2) continue;
1246
- for (const nodeId of layer.sorted) {
1312
+ for (const [i, nodeId] of layer.sorted.entries()) {
1247
1313
  const node = g.getNode(nodeId);
1248
1314
  if (node.index == 0) continue;
1249
1315
  let minGap = Infinity;
1250
1316
  const stack = [];
1317
+ let maxMargin = 0;
1251
1318
  for (const right of _Layout.aligned(g, nodeId, "both")) {
1252
1319
  stack.push(right);
1253
1320
  const leftId = _Layout.leftOf(g, right);
@@ -1256,8 +1323,10 @@ var Layout = class _Layout {
1256
1323
  const leftWidth = left.dims?.[g.w] ?? 0;
1257
1324
  const gap = right.lpos - left.lpos - leftWidth;
1258
1325
  if (gap < minGap) minGap = gap;
1326
+ const margin = right.marginWith(g, left);
1327
+ if (margin > maxMargin) maxMargin = margin;
1259
1328
  }
1260
- const delta = minGap - g.options.nodeMargin;
1329
+ const delta = minGap - maxMargin;
1261
1330
  if (delta <= 0) continue;
1262
1331
  anyChanged = true;
1263
1332
  for (const right of stack)
@@ -1305,9 +1374,8 @@ var Layout = class _Layout {
1305
1374
  };
1306
1375
 
1307
1376
  // src/canvas/marker.tsx
1308
- var import_marker = __toESM(require("./marker.css?raw"), 1);
1309
1377
  var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1310
- function arrow(size, classPrefix, reverse = false) {
1378
+ function arrow(size, reverse = false) {
1311
1379
  const h = size / 1.5;
1312
1380
  const w = size;
1313
1381
  const ry = h / 2;
@@ -1316,7 +1384,7 @@ function arrow(size, classPrefix, reverse = false) {
1316
1384
  "marker",
1317
1385
  {
1318
1386
  id: `g3p-marker-arrow${suffix}`,
1319
- className: `${classPrefix}-marker ${classPrefix}-marker-arrow`,
1387
+ className: "g3p-marker g3p-marker-arrow",
1320
1388
  markerWidth: size,
1321
1389
  markerHeight: size,
1322
1390
  refX: "2",
@@ -1327,7 +1395,7 @@ function arrow(size, classPrefix, reverse = false) {
1327
1395
  }
1328
1396
  );
1329
1397
  }
1330
- function circle(size, classPrefix, reverse = false) {
1398
+ function circle(size, reverse = false) {
1331
1399
  const r = size / 3;
1332
1400
  const cy = size / 2;
1333
1401
  const suffix = reverse ? "-reverse" : "";
@@ -1335,7 +1403,7 @@ function circle(size, classPrefix, reverse = false) {
1335
1403
  "marker",
1336
1404
  {
1337
1405
  id: `g3p-marker-circle${suffix}`,
1338
- className: `${classPrefix}-marker ${classPrefix}-marker-circle`,
1406
+ className: "g3p-marker g3p-marker-circle",
1339
1407
  markerWidth: size,
1340
1408
  markerHeight: size,
1341
1409
  refX: "2",
@@ -1346,7 +1414,7 @@ function circle(size, classPrefix, reverse = false) {
1346
1414
  }
1347
1415
  );
1348
1416
  }
1349
- function diamond(size, classPrefix, reverse = false) {
1417
+ function diamond(size, reverse = false) {
1350
1418
  const w = size * 0.7;
1351
1419
  const h = size / 2;
1352
1420
  const cy = size / 2;
@@ -1355,7 +1423,7 @@ function diamond(size, classPrefix, reverse = false) {
1355
1423
  "marker",
1356
1424
  {
1357
1425
  id: `g3p-marker-diamond${suffix}`,
1358
- className: `${classPrefix}-marker ${classPrefix}-marker-diamond`,
1426
+ className: "g3p-marker g3p-marker-diamond",
1359
1427
  markerWidth: size,
1360
1428
  markerHeight: size,
1361
1429
  refX: "2",
@@ -1366,7 +1434,7 @@ function diamond(size, classPrefix, reverse = false) {
1366
1434
  }
1367
1435
  );
1368
1436
  }
1369
- function bar(size, classPrefix, reverse = false) {
1437
+ function bar(size, reverse = false) {
1370
1438
  const h = size * 0.6;
1371
1439
  const cy = size / 2;
1372
1440
  const suffix = reverse ? "-reverse" : "";
@@ -1374,7 +1442,7 @@ function bar(size, classPrefix, reverse = false) {
1374
1442
  "marker",
1375
1443
  {
1376
1444
  id: `g3p-marker-bar${suffix}`,
1377
- className: `${classPrefix}-marker ${classPrefix}-marker-bar`,
1445
+ className: "g3p-marker g3p-marker-bar",
1378
1446
  markerWidth: size,
1379
1447
  markerHeight: size,
1380
1448
  refX: "2",
@@ -1385,7 +1453,7 @@ function bar(size, classPrefix, reverse = false) {
1385
1453
  }
1386
1454
  );
1387
1455
  }
1388
- function none(size, classPrefix, reverse = false) {
1456
+ function none(size, reverse = false) {
1389
1457
  return void 0;
1390
1458
  }
1391
1459
  function normalize(data) {
@@ -1404,7 +1472,7 @@ var markerDefs = {
1404
1472
  };
1405
1473
 
1406
1474
  // src/graph/services/lines.ts
1407
- var log7 = logger("lines");
1475
+ var log8 = logger("lines");
1408
1476
  var Lines = class _Lines {
1409
1477
  static layoutSeg(g, seg) {
1410
1478
  const sourcePos = Layout.anchorPos(g, seg, "source");
@@ -1450,7 +1518,11 @@ var Lines = class _Lines {
1450
1518
  validTrack = track;
1451
1519
  break;
1452
1520
  }
1453
- if (other.p1 < seg.p2 && seg.p1 < other.p2) {
1521
+ const minA = Math.min(seg.p1, seg.p2);
1522
+ const maxA = Math.max(seg.p1, seg.p2);
1523
+ const minB = Math.min(other.p1, other.p2);
1524
+ const maxB = Math.max(other.p1, other.p2);
1525
+ if (minA < maxB && minB < maxA) {
1454
1526
  overlap = true;
1455
1527
  break;
1456
1528
  }
@@ -1465,6 +1537,21 @@ var Lines = class _Lines {
1465
1537
  else
1466
1538
  trackSet.push([seg]);
1467
1539
  }
1540
+ const midpoint = (s) => (s.p1 + s.p2) / 2;
1541
+ const sortTracks = (tracks2, goingRight) => {
1542
+ tracks2.sort((a, b) => {
1543
+ const midA = Math.max(...a.map(midpoint));
1544
+ const midB = Math.max(...b.map(midpoint));
1545
+ return goingRight ? midB - midA : midA - midB;
1546
+ });
1547
+ };
1548
+ sortTracks(rightTracks, true);
1549
+ sortTracks(leftTracks, false);
1550
+ allTracks.sort((a, b) => {
1551
+ const avgA = a.reduce((sum, s) => sum + midpoint(s), 0) / a.length;
1552
+ const avgB = b.reduce((sum, s) => sum + midpoint(s), 0) / b.length;
1553
+ return avgB - avgA;
1554
+ });
1468
1555
  const tracks = [];
1469
1556
  const all = leftTracks.concat(rightTracks).concat(allTracks);
1470
1557
  for (const track of all)
@@ -1635,6 +1722,7 @@ var Lines = class _Lines {
1635
1722
  };
1636
1723
 
1637
1724
  // src/graph/graph.ts
1725
+ var log9 = logger("graph");
1638
1726
  var emptyChanges = {
1639
1727
  addedNodes: [],
1640
1728
  removedNodes: [],
@@ -1909,29 +1997,8 @@ var screenPos = (x, y) => ({ x, y });
1909
1997
  var canvasPos = (x, y) => ({ x, y });
1910
1998
  var graphPos = (x, y) => ({ x, y });
1911
1999
 
1912
- // src/canvas/node.tsx
1913
- var import_node3 = __toESM(require("./node.css?raw"), 1);
1914
-
1915
- // src/canvas/styler.ts
1916
- var injected = {};
1917
- function styler(name, styles4, prefix) {
1918
- if (prefix === "g3p" && !injected[name]) {
1919
- const style = document.createElement("style");
1920
- style.textContent = styles4;
1921
- document.head.appendChild(style);
1922
- injected[name] = true;
1923
- }
1924
- return (str, condition) => {
1925
- if (!(condition ?? true)) return "";
1926
- const parts = str.split(/\s+/);
1927
- const fixed = parts.map((p) => `${prefix}-${name}-${p}`);
1928
- return fixed.join(" ");
1929
- };
1930
- }
1931
-
1932
2000
  // src/canvas/node.tsx
1933
2001
  var import_jsx_runtime2 = require("jsx-dom/jsx-runtime");
1934
- var log8 = logger("canvas");
1935
2002
  var Node2 = class {
1936
2003
  selected;
1937
2004
  hovered;
@@ -1939,7 +2006,6 @@ var Node2 = class {
1939
2006
  content;
1940
2007
  canvas;
1941
2008
  data;
1942
- classPrefix;
1943
2009
  isDummy;
1944
2010
  pos;
1945
2011
  constructor(canvas, data, isDummy = false) {
@@ -1947,20 +2013,18 @@ var Node2 = class {
1947
2013
  this.data = data;
1948
2014
  this.selected = false;
1949
2015
  this.hovered = false;
1950
- this.classPrefix = canvas.classPrefix;
1951
2016
  this.isDummy = isDummy;
1952
2017
  if (this.isDummy) {
1953
2018
  const size = canvas.dummyNodeSize;
1954
2019
  } else {
1955
2020
  const render = data.render ?? canvas.renderNode;
1956
- this.content = this.renderContent(render(data.data));
2021
+ this.content = this.renderContent(render(data.data, data));
1957
2022
  }
1958
2023
  }
1959
2024
  remove() {
1960
2025
  this.container.remove();
1961
2026
  }
1962
2027
  append() {
1963
- console.log("append", this);
1964
2028
  this.canvas.group.appendChild(this.container);
1965
2029
  }
1966
2030
  needsContentSize() {
@@ -1969,19 +2033,6 @@ var Node2 = class {
1969
2033
  needsContainerSize() {
1970
2034
  return !this.isDummy;
1971
2035
  }
1972
- handleClick(e) {
1973
- e.stopPropagation();
1974
- }
1975
- handleMouseEnter(e) {
1976
- }
1977
- handleMouseLeave(e) {
1978
- }
1979
- handleContextMenu(e) {
1980
- }
1981
- handleMouseDown(e) {
1982
- }
1983
- handleMouseUp(e) {
1984
- }
1985
2036
  setPos(pos) {
1986
2037
  this.pos = pos;
1987
2038
  const { x, y } = pos;
@@ -1998,7 +2049,6 @@ var Node2 = class {
1998
2049
  return el;
1999
2050
  }
2000
2051
  renderContainer() {
2001
- const c = styler("node", import_node3.default, this.classPrefix);
2002
2052
  const hasPorts = this.hasPorts();
2003
2053
  const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
2004
2054
  const nodeType = this.data?.type;
@@ -2006,14 +2056,8 @@ var Node2 = class {
2006
2056
  this.container = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2007
2057
  "g",
2008
2058
  {
2009
- className: `${c("container")} ${c("dummy", this.isDummy)} ${typeClass}`.trim(),
2010
- onClick: (e) => this.handleClick(e),
2011
- onMouseEnter: (e) => this.handleMouseEnter(e),
2012
- onMouseLeave: (e) => this.handleMouseLeave(e),
2013
- onContextMenu: (e) => this.handleContextMenu(e),
2014
- onMouseDown: (e) => this.handleMouseDown(e),
2015
- onMouseUp: (e) => this.handleMouseUp(e),
2016
- style: { cursor: "pointer" },
2059
+ className: `g3p-node-container ${this.isDummy ? "g3p-node-dummy" : ""} ${typeClass}`.trim(),
2060
+ "data-node-id": this.data?.id,
2017
2061
  children: inner
2018
2062
  }
2019
2063
  );
@@ -2023,7 +2067,6 @@ var Node2 = class {
2023
2067
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("foreignObject", { width: w, height: h, children: this.content });
2024
2068
  }
2025
2069
  renderDummy() {
2026
- const c = styler("node", import_node3.default, this.classPrefix);
2027
2070
  let w = this.canvas.dummyNodeSize;
2028
2071
  let h = this.canvas.dummyNodeSize;
2029
2072
  w /= 2;
@@ -2036,7 +2079,7 @@ var Node2 = class {
2036
2079
  cy: h,
2037
2080
  rx: w,
2038
2081
  ry: h,
2039
- className: c("background")
2082
+ className: "g3p-node-background"
2040
2083
  }
2041
2084
  ),
2042
2085
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -2047,7 +2090,7 @@ var Node2 = class {
2047
2090
  rx: w,
2048
2091
  ry: h,
2049
2092
  fill: "none",
2050
- className: c("border")
2093
+ className: "g3p-node-border"
2051
2094
  }
2052
2095
  )
2053
2096
  ] });
@@ -2060,7 +2103,7 @@ var Node2 = class {
2060
2103
  const ports = data.ports?.[dir];
2061
2104
  if (!ports) continue;
2062
2105
  for (const port of ports) {
2063
- const el = this.content.querySelector(`#g3p-port-${data.id}-${port.id}`);
2106
+ const el = this.content.querySelector(`.g3p-node-port[data-node-id="${data.id}"][data-port-id="${port.id}"]`);
2064
2107
  if (!el) continue;
2065
2108
  const portRect = el.getBoundingClientRect();
2066
2109
  if (isVertical) {
@@ -2098,23 +2141,22 @@ var Node2 = class {
2098
2141
  renderPortRow(dir, inout) {
2099
2142
  const ports = this.data?.ports?.[dir];
2100
2143
  if (!ports?.length) return null;
2101
- const c = styler("node", import_node3.default, this.classPrefix);
2102
2144
  const pos = this.getPortPosition(dir);
2103
2145
  const isVertical = this.isVerticalOrientation();
2104
2146
  const layoutClass = isVertical ? "row" : "col";
2105
2147
  const rotateLabels = false;
2106
2148
  const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2107
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `${c("ports")} ${c(`ports-${layoutClass}`)}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2149
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `g3p-node-ports g3p-node-ports-${layoutClass}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2108
2150
  "div",
2109
2151
  {
2110
- id: `g3p-port-${this.data.id}-${port.id}`,
2111
- className: `${c("port")} ${c(`port-${inout}-${pos}`)} ${c(rotateClass)}`,
2152
+ className: `g3p-node-port g3p-node-port-${inout}-${pos} ${rotateClass}`,
2153
+ "data-node-id": this.data.id,
2154
+ "data-port-id": port.id,
2112
2155
  children: port.label ?? port.id
2113
2156
  }
2114
2157
  )) });
2115
2158
  }
2116
2159
  renderInsidePorts(el) {
2117
- const c = styler("node", import_node3.default, this.classPrefix);
2118
2160
  const isVertical = this.isVerticalOrientation();
2119
2161
  const isReversed = this.isReversedOrientation();
2120
2162
  let inPorts = this.renderPortRow("in", "in");
@@ -2122,14 +2164,13 @@ var Node2 = class {
2122
2164
  if (!inPorts && !outPorts) return el;
2123
2165
  if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2124
2166
  const wrapperClass = isVertical ? "v" : "h";
2125
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2167
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2126
2168
  inPorts,
2127
2169
  el,
2128
2170
  outPorts
2129
2171
  ] });
2130
2172
  }
2131
2173
  renderOutsidePorts(el) {
2132
- const c = styler("node", import_node3.default, this.classPrefix);
2133
2174
  const isVertical = this.isVerticalOrientation();
2134
2175
  const isReversed = this.isReversedOrientation();
2135
2176
  let inPorts = this.renderPortRow("in", "out");
@@ -2137,71 +2178,59 @@ var Node2 = class {
2137
2178
  if (!inPorts && !outPorts) return el;
2138
2179
  if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2139
2180
  const wrapperClass = isVertical ? "v" : "h";
2140
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2181
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2141
2182
  inPorts,
2142
2183
  el,
2143
2184
  outPorts
2144
2185
  ] });
2145
2186
  }
2146
2187
  renderBorder(el) {
2147
- const c = styler("node", import_node3.default, this.classPrefix);
2148
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: c("border"), children: el });
2188
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "g3p-node-border", children: el });
2149
2189
  }
2150
2190
  };
2151
2191
 
2152
2192
  // src/canvas/seg.tsx
2153
- var import_seg3 = __toESM(require("./seg.css?raw"), 1);
2154
2193
  var import_jsx_runtime3 = require("jsx-dom/jsx-runtime");
2155
- var log9 = logger("canvas");
2156
2194
  var Seg2 = class {
2157
2195
  id;
2158
2196
  selected;
2159
2197
  hovered;
2160
2198
  canvas;
2161
- classPrefix;
2162
2199
  type;
2163
2200
  svg;
2164
2201
  el;
2165
2202
  source;
2166
2203
  target;
2204
+ edgeIds;
2167
2205
  constructor(canvas, data, g) {
2168
2206
  this.id = data.id;
2169
2207
  this.canvas = canvas;
2170
2208
  this.selected = false;
2171
2209
  this.hovered = false;
2172
2210
  this.svg = data.svg;
2173
- this.classPrefix = canvas.classPrefix;
2174
2211
  this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2175
2212
  this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2176
2213
  this.type = data.type;
2214
+ this.edgeIds = data.edgeIds.toArray();
2177
2215
  this.el = this.render();
2178
2216
  }
2179
- handleClick(e) {
2180
- e.stopPropagation();
2181
- }
2182
- handleMouseEnter(e) {
2183
- }
2184
- handleMouseLeave(e) {
2185
- }
2186
- handleContextMenu(e) {
2187
- }
2188
2217
  append() {
2189
2218
  this.canvas.group.appendChild(this.el);
2190
2219
  }
2191
2220
  remove() {
2192
2221
  this.el.remove();
2193
2222
  }
2194
- update(data) {
2223
+ update(data, g) {
2195
2224
  this.svg = data.svg;
2196
2225
  this.type = data.type;
2197
- this.source = data.source;
2198
- this.target = data.target;
2226
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2227
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2228
+ this.edgeIds = data.edgeIds.toArray();
2199
2229
  this.remove();
2200
2230
  this.el = this.render();
2201
2231
  this.append();
2202
2232
  }
2203
2233
  render() {
2204
- const c = styler("seg", import_seg3.default, this.classPrefix);
2205
2234
  let { source, target } = normalize(this);
2206
2235
  if (this.source.isDummy) source = void 0;
2207
2236
  if (this.target.isDummy) target = void 0;
@@ -2211,18 +2240,15 @@ var Seg2 = class {
2211
2240
  {
2212
2241
  ref: (el) => this.el = el,
2213
2242
  id: `g3p-seg-${this.id}`,
2214
- className: `${c("container")} ${typeClass}`.trim(),
2215
- onClick: this.handleClick.bind(this),
2216
- onMouseEnter: this.handleMouseEnter.bind(this),
2217
- onMouseLeave: this.handleMouseLeave.bind(this),
2218
- onContextMenu: this.handleContextMenu.bind(this),
2243
+ className: `g3p-seg-container ${typeClass}`.trim(),
2244
+ "data-edge-id": this.id,
2219
2245
  children: [
2220
2246
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2221
2247
  "path",
2222
2248
  {
2223
2249
  d: this.svg,
2224
2250
  fill: "none",
2225
- className: c("line"),
2251
+ className: "g3p-seg-line",
2226
2252
  markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2227
2253
  markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2228
2254
  }
@@ -2233,7 +2259,7 @@ var Seg2 = class {
2233
2259
  d: this.svg,
2234
2260
  stroke: "transparent",
2235
2261
  fill: "none",
2236
- className: c("hitbox"),
2262
+ className: "g3p-seg-hitbox",
2237
2263
  style: { cursor: "pointer" }
2238
2264
  }
2239
2265
  )
@@ -2243,46 +2269,561 @@ var Seg2 = class {
2243
2269
  }
2244
2270
  };
2245
2271
 
2246
- // src/canvas/canvas.tsx
2247
- var import_canvas = __toESM(require("./canvas.css?raw"), 1);
2248
- var import_zoom = __toESM(require("./zoom.css?raw"), 1);
2272
+ // src/canvas/editMode.ts
2273
+ var EditMode = class {
2274
+ _state = { type: "idle" };
2275
+ _editable = false;
2276
+ get state() {
2277
+ return this._state;
2278
+ }
2279
+ get editable() {
2280
+ return this._editable;
2281
+ }
2282
+ set editable(value) {
2283
+ this._editable = value;
2284
+ if (!value) {
2285
+ this.reset();
2286
+ }
2287
+ }
2288
+ get isIdle() {
2289
+ return this._state.type === "idle";
2290
+ }
2291
+ get isPanning() {
2292
+ return this._state.type === "panning";
2293
+ }
2294
+ get isCreatingEdge() {
2295
+ return this._state.type === "new-edge";
2296
+ }
2297
+ /** Start panning the canvas */
2298
+ startPan(startCanvas, startTransform) {
2299
+ this._state = { type: "panning", startCanvas, startTransform };
2300
+ }
2301
+ /** Start creating a new edge from a node or port */
2302
+ startNewEdge(id, startGraph, port) {
2303
+ if (!this._editable) return;
2304
+ this._state = {
2305
+ type: "new-edge",
2306
+ source: { id, port },
2307
+ startGraph,
2308
+ currentGraph: startGraph,
2309
+ target: null
2310
+ };
2311
+ }
2312
+ /** Update the current position during new-edge mode */
2313
+ updateNewEdgePosition(currentGraph) {
2314
+ if (this._state.type === "new-edge") {
2315
+ this._state = { ...this._state, currentGraph };
2316
+ }
2317
+ }
2318
+ /** Update the hover target during new-edge mode */
2319
+ setHoverTarget(target) {
2320
+ if (this._state.type === "new-edge") {
2321
+ this._state = { ...this._state, target };
2322
+ }
2323
+ }
2324
+ /** Get the new-edge state if active */
2325
+ getNewEdgeState() {
2326
+ if (this._state.type === "new-edge") {
2327
+ return this._state;
2328
+ }
2329
+ return null;
2330
+ }
2331
+ /** Reset to idle state */
2332
+ reset() {
2333
+ this._state = { type: "idle" };
2334
+ }
2335
+ };
2336
+
2337
+ // src/canvas/newEdge.tsx
2249
2338
  var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
2250
- var log10 = logger("canvas");
2251
- var themeVarMap = {
2252
- // Canvas
2253
- bg: "--g3p-bg",
2254
- shadow: "--g3p-shadow",
2255
- // Node
2256
- border: "--g3p-border",
2257
- borderHover: "--g3p-border-hover",
2258
- borderSelected: "--g3p-border-selected",
2259
- text: "--g3p-text",
2260
- textMuted: "--g3p-text-muted",
2261
- // Port
2262
- bgHover: "--g3p-port-bg-hover",
2263
- // Edge
2264
- color: "--g3p-edge-color"
2339
+ function renderNewEdge({ start, end }) {
2340
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: "g3p-new-edge-container", children: [
2341
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2342
+ "circle",
2343
+ {
2344
+ cx: start.x,
2345
+ cy: start.y,
2346
+ r: 4,
2347
+ className: "g3p-new-edge-origin"
2348
+ }
2349
+ ),
2350
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2351
+ "line",
2352
+ {
2353
+ x1: start.x,
2354
+ y1: start.y,
2355
+ x2: end.x,
2356
+ y2: end.y,
2357
+ className: "g3p-new-edge-line"
2358
+ }
2359
+ ),
2360
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2361
+ "circle",
2362
+ {
2363
+ cx: end.x,
2364
+ cy: end.y,
2365
+ r: 3,
2366
+ className: "g3p-new-edge-end"
2367
+ }
2368
+ )
2369
+ ] });
2370
+ }
2371
+
2372
+ // src/canvas/modal.tsx
2373
+ var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2374
+ var Modal = class {
2375
+ container;
2376
+ overlay;
2377
+ dialog;
2378
+ onClose;
2379
+ mouseDownOnOverlay = false;
2380
+ constructor(options) {
2381
+ this.onClose = options.onClose;
2382
+ this.overlay = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2383
+ "div",
2384
+ {
2385
+ className: "g3p-modal-overlay",
2386
+ onMouseDown: (e) => {
2387
+ this.mouseDownOnOverlay = e.target === this.overlay;
2388
+ },
2389
+ onMouseUp: (e) => {
2390
+ if (this.mouseDownOnOverlay && e.target === this.overlay) this.close();
2391
+ this.mouseDownOnOverlay = false;
2392
+ }
2393
+ }
2394
+ );
2395
+ this.dialog = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-dialog", children: [
2396
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-header", children: [
2397
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "g3p-modal-title", children: options.title }),
2398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2399
+ "button",
2400
+ {
2401
+ className: "g3p-modal-close",
2402
+ onClick: () => this.close(),
2403
+ children: "\xD7"
2404
+ }
2405
+ )
2406
+ ] }),
2407
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-body" }),
2408
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-footer" })
2409
+ ] });
2410
+ if (options.position) {
2411
+ this.dialog.style.position = "absolute";
2412
+ this.dialog.style.left = `${options.position.x}px`;
2413
+ this.dialog.style.top = `${options.position.y}px`;
2414
+ this.dialog.style.transform = "translate(-50%, -50%)";
2415
+ }
2416
+ this.overlay.appendChild(this.dialog);
2417
+ this.container = this.overlay;
2418
+ this.handleKeyDown = this.handleKeyDown.bind(this);
2419
+ document.addEventListener("keydown", this.handleKeyDown);
2420
+ }
2421
+ handleKeyDown(e) {
2422
+ if (e.key === "Escape") {
2423
+ this.close();
2424
+ }
2425
+ }
2426
+ get body() {
2427
+ return this.dialog.querySelector(".g3p-modal-body");
2428
+ }
2429
+ get footer() {
2430
+ return this.dialog.querySelector(".g3p-modal-footer");
2431
+ }
2432
+ show(parent) {
2433
+ parent.appendChild(this.container);
2434
+ const firstInput = this.dialog.querySelector("input, select, button");
2435
+ if (firstInput) firstInput.focus();
2436
+ }
2437
+ close() {
2438
+ document.removeEventListener("keydown", this.handleKeyDown);
2439
+ this.container.remove();
2440
+ this.onClose();
2441
+ }
2265
2442
  };
2266
- function themeToCSS(theme, selector, prefix = "") {
2267
- const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
2268
- if (!entries.length) return "";
2269
- let css = `${selector} {
2270
- `;
2271
- for (const [key, value] of entries) {
2272
- let cssVar = themeVarMap[key];
2273
- if (key === "bg" && prefix === "node") {
2274
- cssVar = "--g3p-bg-node";
2275
- } else if (key === "bg" && prefix === "port") {
2276
- cssVar = "--g3p-port-bg";
2443
+ var NewNodeModal = class extends Modal {
2444
+ fieldInputs = /* @__PURE__ */ new Map();
2445
+ typeSelect;
2446
+ submitCallback;
2447
+ fields;
2448
+ constructor(options) {
2449
+ super({
2450
+ title: "New Node",
2451
+ onClose: () => options.onCancel?.()
2452
+ });
2453
+ this.submitCallback = options.onSubmit;
2454
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2455
+ this.renderBody(options.nodeTypes);
2456
+ this.renderFooter();
2457
+ }
2458
+ renderBody(nodeTypes) {
2459
+ this.body.innerHTML = "";
2460
+ for (const [name, type] of this.fields) {
2461
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2462
+ const fieldGroup = this.renderField(name, label, type);
2463
+ this.body.appendChild(fieldGroup);
2277
2464
  }
2278
- if (cssVar) {
2279
- css += ` ${cssVar}: ${value};
2280
- `;
2465
+ if (nodeTypes && nodeTypes.length > 0) {
2466
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2467
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2468
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2469
+ "select",
2470
+ {
2471
+ className: "g3p-modal-select",
2472
+ ref: (el) => this.typeSelect = el,
2473
+ children: [
2474
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Default" }),
2475
+ nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, children: type }))
2476
+ ]
2477
+ }
2478
+ )
2479
+ ] });
2480
+ this.body.appendChild(typeGroup);
2281
2481
  }
2282
2482
  }
2283
- css += "}\n";
2284
- return css;
2285
- }
2483
+ renderField(name, label, type) {
2484
+ if (type === "boolean") {
2485
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "g3p-modal-label", children: [
2486
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2487
+ "input",
2488
+ {
2489
+ type: "checkbox",
2490
+ className: "g3p-modal-checkbox",
2491
+ ref: (el) => this.fieldInputs.set(name, el)
2492
+ }
2493
+ ),
2494
+ label
2495
+ ] }) });
2496
+ }
2497
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2498
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
2499
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2500
+ "input",
2501
+ {
2502
+ type: type === "number" ? "number" : "text",
2503
+ className: "g3p-modal-input",
2504
+ placeholder: `Enter ${label.toLowerCase()}`,
2505
+ ref: (el) => this.fieldInputs.set(name, el)
2506
+ }
2507
+ )
2508
+ ] });
2509
+ }
2510
+ renderFooter() {
2511
+ this.footer.innerHTML = "";
2512
+ this.footer.appendChild(
2513
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2514
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2515
+ "button",
2516
+ {
2517
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2518
+ onClick: () => this.close(),
2519
+ children: "Cancel"
2520
+ }
2521
+ ),
2522
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2523
+ "button",
2524
+ {
2525
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2526
+ onClick: () => this.submit(),
2527
+ children: "Create"
2528
+ }
2529
+ )
2530
+ ] })
2531
+ );
2532
+ }
2533
+ submit() {
2534
+ const data = {};
2535
+ for (const [name, type] of this.fields) {
2536
+ const input = this.fieldInputs.get(name);
2537
+ if (!input) continue;
2538
+ if (type === "boolean") {
2539
+ data[name] = input.checked;
2540
+ } else if (type === "number") {
2541
+ const val = input.value;
2542
+ if (val) data[name] = Number(val);
2543
+ } else {
2544
+ const val = input.value.trim();
2545
+ if (val) data[name] = val;
2546
+ }
2547
+ }
2548
+ if (Object.keys(data).length === 0) {
2549
+ const firstInput = this.fieldInputs.values().next().value;
2550
+ if (firstInput) firstInput.focus();
2551
+ return;
2552
+ }
2553
+ if (this.typeSelect?.value) {
2554
+ data.type = this.typeSelect.value;
2555
+ }
2556
+ document.removeEventListener("keydown", this.handleKeyDown);
2557
+ this.container.remove();
2558
+ this.submitCallback(data);
2559
+ }
2560
+ };
2561
+ var EditNodeModal = class extends Modal {
2562
+ fieldInputs = /* @__PURE__ */ new Map();
2563
+ typeSelect;
2564
+ node;
2565
+ fields;
2566
+ submitCallback;
2567
+ deleteCallback;
2568
+ constructor(options) {
2569
+ super({
2570
+ title: "Edit Node",
2571
+ position: options.position,
2572
+ onClose: () => options.onCancel?.()
2573
+ });
2574
+ this.node = options.node;
2575
+ this.submitCallback = options.onSubmit;
2576
+ this.deleteCallback = options.onDelete;
2577
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2578
+ if (!options.fields && !this.node.title)
2579
+ this.node = { ...this.node, title: this.node.id };
2580
+ this.renderBody(options.nodeTypes);
2581
+ this.renderFooter();
2582
+ }
2583
+ renderBody(nodeTypes) {
2584
+ console.log(`renderBody`, this.node);
2585
+ this.body.innerHTML = "";
2586
+ for (const [name, type] of this.fields) {
2587
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2588
+ const currentValue = this.node[name];
2589
+ const fieldGroup = this.renderField(name, label, type, currentValue);
2590
+ this.body.appendChild(fieldGroup);
2591
+ }
2592
+ if (nodeTypes && nodeTypes.length > 0) {
2593
+ const currentType = this.node.type ?? "";
2594
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2595
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2596
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2597
+ "select",
2598
+ {
2599
+ className: "g3p-modal-select",
2600
+ ref: (el) => this.typeSelect = el,
2601
+ children: [
2602
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
2603
+ nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
2604
+ ]
2605
+ }
2606
+ )
2607
+ ] });
2608
+ this.body.appendChild(typeGroup);
2609
+ }
2610
+ }
2611
+ renderField(name, label, type, currentValue) {
2612
+ if (type === "boolean") {
2613
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "g3p-modal-label", children: [
2614
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2615
+ "input",
2616
+ {
2617
+ type: "checkbox",
2618
+ className: "g3p-modal-checkbox",
2619
+ checked: !!currentValue,
2620
+ ref: (el) => this.fieldInputs.set(name, el)
2621
+ }
2622
+ ),
2623
+ label
2624
+ ] }) });
2625
+ }
2626
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2627
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
2628
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2629
+ "input",
2630
+ {
2631
+ type: type === "number" ? "number" : "text",
2632
+ className: "g3p-modal-input",
2633
+ value: currentValue ?? "",
2634
+ ref: (el) => this.fieldInputs.set(name, el)
2635
+ }
2636
+ )
2637
+ ] });
2638
+ }
2639
+ renderFooter() {
2640
+ this.footer.innerHTML = "";
2641
+ this.footer.appendChild(
2642
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2643
+ this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2644
+ "button",
2645
+ {
2646
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2647
+ onClick: () => this.delete(),
2648
+ children: "Delete"
2649
+ }
2650
+ ),
2651
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
2652
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2653
+ "button",
2654
+ {
2655
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2656
+ onClick: () => this.close(),
2657
+ children: "Cancel"
2658
+ }
2659
+ ),
2660
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2661
+ "button",
2662
+ {
2663
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2664
+ onClick: () => this.submit(),
2665
+ children: "Save"
2666
+ }
2667
+ )
2668
+ ] })
2669
+ );
2670
+ }
2671
+ submit() {
2672
+ const data = { ...this.node };
2673
+ for (const [name, type] of this.fields) {
2674
+ const input = this.fieldInputs.get(name);
2675
+ if (!input) continue;
2676
+ if (type === "boolean") {
2677
+ data[name] = input.checked;
2678
+ } else if (type === "number") {
2679
+ const val = input.value;
2680
+ data[name] = val ? Number(val) : void 0;
2681
+ } else {
2682
+ const val = input.value.trim();
2683
+ data[name] = val || void 0;
2684
+ }
2685
+ }
2686
+ if (this.typeSelect) {
2687
+ data.type = this.typeSelect.value || void 0;
2688
+ }
2689
+ document.removeEventListener("keydown", this.handleKeyDown);
2690
+ this.container.remove();
2691
+ this.submitCallback(data);
2692
+ }
2693
+ delete() {
2694
+ document.removeEventListener("keydown", this.handleKeyDown);
2695
+ this.container.remove();
2696
+ this.deleteCallback?.();
2697
+ }
2698
+ };
2699
+ var EditEdgeModal = class _EditEdgeModal extends Modal {
2700
+ sourceMarkerSelect;
2701
+ targetMarkerSelect;
2702
+ typeSelect;
2703
+ edge;
2704
+ submitCallback;
2705
+ deleteCallback;
2706
+ static markerTypes = ["none", "arrow", "circle", "diamond", "bar"];
2707
+ constructor(options) {
2708
+ super({
2709
+ title: "Edit Edge",
2710
+ onClose: () => options.onCancel?.()
2711
+ });
2712
+ this.edge = options.edge;
2713
+ this.submitCallback = options.onSubmit;
2714
+ this.deleteCallback = options.onDelete;
2715
+ this.renderBody(options.edgeTypes);
2716
+ this.renderFooter();
2717
+ }
2718
+ renderBody(edgeTypes) {
2719
+ this.body.innerHTML = "";
2720
+ const currentSourceMarker = this.edge.source?.marker ?? "none";
2721
+ const currentTargetMarker = this.edge.target?.marker ?? "arrow";
2722
+ const sourceGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2723
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Source Marker" }),
2724
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2725
+ "select",
2726
+ {
2727
+ className: "g3p-modal-select",
2728
+ ref: (el) => this.sourceMarkerSelect = el,
2729
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentSourceMarker, children: type }))
2730
+ }
2731
+ )
2732
+ ] });
2733
+ this.body.appendChild(sourceGroup);
2734
+ const targetGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2735
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Target Marker" }),
2736
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2737
+ "select",
2738
+ {
2739
+ className: "g3p-modal-select",
2740
+ ref: (el) => this.targetMarkerSelect = el,
2741
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentTargetMarker, children: type }))
2742
+ }
2743
+ )
2744
+ ] });
2745
+ this.body.appendChild(targetGroup);
2746
+ if (edgeTypes && edgeTypes.length > 0) {
2747
+ const currentType = this.edge.type ?? "";
2748
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2749
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2750
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2751
+ "select",
2752
+ {
2753
+ className: "g3p-modal-select",
2754
+ ref: (el) => this.typeSelect = el,
2755
+ children: [
2756
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
2757
+ edgeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
2758
+ ]
2759
+ }
2760
+ )
2761
+ ] });
2762
+ this.body.appendChild(typeGroup);
2763
+ }
2764
+ }
2765
+ renderFooter() {
2766
+ this.footer.innerHTML = "";
2767
+ this.footer.appendChild(
2768
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2769
+ this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2770
+ "button",
2771
+ {
2772
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2773
+ onClick: () => this.delete(),
2774
+ children: "Delete"
2775
+ }
2776
+ ),
2777
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
2778
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2779
+ "button",
2780
+ {
2781
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2782
+ onClick: () => this.close(),
2783
+ children: "Cancel"
2784
+ }
2785
+ ),
2786
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2787
+ "button",
2788
+ {
2789
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2790
+ onClick: () => this.submit(),
2791
+ children: "Save"
2792
+ }
2793
+ )
2794
+ ] })
2795
+ );
2796
+ }
2797
+ submit() {
2798
+ const data = {
2799
+ ...this.edge,
2800
+ source: {
2801
+ ...this.edge.source,
2802
+ marker: this.sourceMarkerSelect.value === "none" ? void 0 : this.sourceMarkerSelect.value
2803
+ },
2804
+ target: {
2805
+ ...this.edge.target,
2806
+ marker: this.targetMarkerSelect.value === "none" ? void 0 : this.targetMarkerSelect.value
2807
+ }
2808
+ };
2809
+ if (this.typeSelect) {
2810
+ data.type = this.typeSelect.value || void 0;
2811
+ }
2812
+ document.removeEventListener("keydown", this.handleKeyDown);
2813
+ this.container.remove();
2814
+ this.submitCallback(data);
2815
+ }
2816
+ delete() {
2817
+ document.removeEventListener("keydown", this.handleKeyDown);
2818
+ this.container.remove();
2819
+ this.deleteCallback?.();
2820
+ }
2821
+ };
2822
+
2823
+ // src/canvas/canvas.tsx
2824
+ var import_styles = __toESM(require("./styles.css?raw"), 1);
2825
+ var import_jsx_runtime6 = require("jsx-dom/jsx-runtime");
2826
+ var log10 = logger("canvas");
2286
2827
  var Canvas = class {
2287
2828
  container;
2288
2829
  root;
@@ -2295,19 +2836,26 @@ var Canvas = class {
2295
2836
  curSegs;
2296
2837
  updating;
2297
2838
  // Pan-zoom state
2298
- isPanning = false;
2299
- panStart = null;
2300
- transformStart = null;
2301
2839
  panScale = null;
2302
2840
  zoomControls;
2303
- constructor(options) {
2841
+ // Edit mode state machine
2842
+ editMode;
2843
+ api;
2844
+ // New-edge visual element
2845
+ newEdgeEl;
2846
+ // Pending drag state (for double-click debounce)
2847
+ pendingDrag = null;
2848
+ constructor(api, options) {
2304
2849
  Object.assign(this, options);
2850
+ this.api = api;
2305
2851
  this.allNodes = /* @__PURE__ */ new Map();
2306
2852
  this.curNodes = /* @__PURE__ */ new Map();
2307
2853
  this.curSegs = /* @__PURE__ */ new Map();
2308
2854
  this.updating = false;
2309
2855
  this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2310
2856
  this.transform = { x: 0, y: 0, scale: 1 };
2857
+ this.editMode = new EditMode();
2858
+ this.editMode.editable = this.editable;
2311
2859
  this.createMeasurementContainer();
2312
2860
  this.createCanvasContainer();
2313
2861
  if (this.panZoom) this.setupPanZoom();
@@ -2352,7 +2900,6 @@ var Canvas = class {
2352
2900
  if (gnode.isDummy) {
2353
2901
  node = new Node2(this, gnode, true);
2354
2902
  node.renderContainer();
2355
- node.setPos(gnode.pos);
2356
2903
  this.allNodes.set(key, node);
2357
2904
  } else {
2358
2905
  if (!this.allNodes.has(key))
@@ -2361,6 +2908,7 @@ var Canvas = class {
2361
2908
  }
2362
2909
  this.curNodes.set(gnode.id, node);
2363
2910
  node.append();
2911
+ node.setPos(gnode.pos);
2364
2912
  }
2365
2913
  updateNode(gnode) {
2366
2914
  if (gnode.isDummy) throw new Error("dummy node cannot be updated");
@@ -2382,10 +2930,10 @@ var Canvas = class {
2382
2930
  this.curSegs.set(gseg.id, seg);
2383
2931
  seg.append();
2384
2932
  }
2385
- updateSeg(gseg) {
2933
+ updateSeg(gseg, g) {
2386
2934
  const seg = this.curSegs.get(gseg.id);
2387
2935
  if (!seg) throw new Error("seg not found");
2388
- seg.update(gseg);
2936
+ seg.update(gseg, g);
2389
2937
  }
2390
2938
  deleteSeg(gseg) {
2391
2939
  const seg = this.curSegs.get(gseg.id);
@@ -2405,18 +2953,82 @@ var Canvas = class {
2405
2953
  for (const node of newNodes.values()) {
2406
2954
  node.measure(isVertical);
2407
2955
  const { id, version } = node.data;
2408
- const key = `${id}:${version}`;
2956
+ const key = `k:${id}:${version}`;
2409
2957
  this.allNodes.set(key, node);
2410
2958
  node.renderContainer();
2411
2959
  }
2412
2960
  this.measurement.innerHTML = "";
2413
2961
  return newNodes;
2414
2962
  }
2963
+ // ========== Mouse event handlers ==========
2415
2964
  onClick(e) {
2416
- console.log("click", e);
2965
+ const hit = this.hitTest(e.clientX, e.clientY);
2966
+ if (hit.type === "node") {
2967
+ this.api.handleClickNode(hit.node.data.id);
2968
+ } else if (hit.type === "edge") {
2969
+ this.api.handleClickEdge(hit.segId);
2970
+ }
2971
+ }
2972
+ onDoubleClick(e) {
2973
+ if (this.pendingDrag) {
2974
+ window.clearTimeout(this.pendingDrag.timeout);
2975
+ this.pendingDrag = null;
2976
+ }
2977
+ if (!this.editMode.editable) return;
2978
+ const hit = this.hitTest(e.clientX, e.clientY);
2979
+ if (hit.type === "node") {
2980
+ if (hit.node.isDummy) return;
2981
+ this.api.handleEditNode(hit.node.data.id);
2982
+ } else if (hit.type === "edge") {
2983
+ this.api.handleEditEdge(hit.segId);
2984
+ } else {
2985
+ this.api.handleNewNode();
2986
+ }
2987
+ }
2988
+ // ========== Built-in Modals ==========
2989
+ /** Show the new node modal */
2990
+ showNewNodeModal(callback) {
2991
+ const nodeTypes = Object.keys(this.nodeTypes);
2992
+ const fields = this.api.getNodeFields();
2993
+ const modal = new NewNodeModal({
2994
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
2995
+ fields: fields.size > 0 ? fields : void 0,
2996
+ onSubmit: (data) => {
2997
+ callback(data);
2998
+ }
2999
+ });
3000
+ modal.show(document.body);
3001
+ }
3002
+ /** Show the edit node modal */
3003
+ showEditNodeModal(node, callback) {
3004
+ const nodeTypes = Object.keys(this.nodeTypes);
3005
+ const fields = this.api.getNodeFields();
3006
+ const modal = new EditNodeModal({
3007
+ node,
3008
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
3009
+ fields: fields.size > 0 ? fields : void 0,
3010
+ onSubmit: (data) => {
3011
+ callback(data);
3012
+ },
3013
+ onDelete: () => {
3014
+ this.api.handleDeleteNode(node.id);
3015
+ }
3016
+ });
3017
+ modal.show(document.body);
3018
+ }
3019
+ /** Show the edit edge modal */
3020
+ showEditEdgeModal(edge, callback) {
3021
+ const modal = new EditEdgeModal({
3022
+ edge,
3023
+ edgeTypes: Object.keys(this.edgeTypes),
3024
+ onSubmit: callback,
3025
+ onDelete: () => {
3026
+ this.api.handleDeleteEdge(edge.id);
3027
+ }
3028
+ });
3029
+ modal.show(document.body);
2417
3030
  }
2418
3031
  onContextMenu(e) {
2419
- console.log("context menu", e);
2420
3032
  }
2421
3033
  groupTransform() {
2422
3034
  return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
@@ -2431,53 +3043,52 @@ var Canvas = class {
2431
3043
  }
2432
3044
  generateDynamicStyles() {
2433
3045
  let css = "";
2434
- const prefix = this.classPrefix;
2435
- css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
3046
+ css += themeToCSS(this.theme, `.g3p-canvas-container`);
2436
3047
  for (const [type, vars] of Object.entries(this.nodeTypes)) {
2437
- css += themeToCSS(vars, `.${prefix}-node-type-${type}`, "node");
3048
+ css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
2438
3049
  }
2439
3050
  for (const [type, vars] of Object.entries(this.edgeTypes)) {
2440
- css += themeToCSS(vars, `.${prefix}-edge-type-${type}`);
3051
+ css += themeToCSS(vars, `.g3p-edge-type-${type}`);
2441
3052
  }
2442
3053
  return css;
2443
3054
  }
2444
3055
  createCanvasContainer() {
2445
- const markerStyleEl = document.createElement("style");
2446
- markerStyleEl.textContent = import_marker.default;
2447
- document.head.appendChild(markerStyleEl);
2448
- const zoomStyleEl = document.createElement("style");
2449
- zoomStyleEl.textContent = import_zoom.default;
2450
- document.head.appendChild(zoomStyleEl);
3056
+ if (!document.getElementById("g3p-styles")) {
3057
+ const baseStyleEl = document.createElement("style");
3058
+ baseStyleEl.id = "g3p-styles";
3059
+ baseStyleEl.textContent = import_styles.default;
3060
+ document.head.appendChild(baseStyleEl);
3061
+ }
2451
3062
  const dynamicStyles = this.generateDynamicStyles();
2452
3063
  if (dynamicStyles) {
2453
- const themeStyleEl = document.createElement("style");
2454
- themeStyleEl.textContent = dynamicStyles;
2455
- document.head.appendChild(themeStyleEl);
3064
+ const dynamicStyleEl = document.createElement("style");
3065
+ dynamicStyleEl.textContent = dynamicStyles;
3066
+ document.head.appendChild(dynamicStyleEl);
2456
3067
  }
2457
- const c = styler("canvas", import_canvas.default, this.classPrefix);
2458
3068
  const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
2459
- this.container = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3069
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2460
3070
  "div",
2461
3071
  {
2462
- className: `${c("container")} ${colorModeClass}`.trim(),
3072
+ className: `g3p-canvas-container ${colorModeClass}`.trim(),
2463
3073
  ref: (el) => this.container = el,
2464
3074
  onContextMenu: this.onContextMenu.bind(this),
2465
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3075
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2466
3076
  "svg",
2467
3077
  {
2468
3078
  ref: (el) => this.root = el,
2469
- className: c("root"),
3079
+ className: "g3p-canvas-root",
2470
3080
  width: this.width,
2471
3081
  height: this.height,
2472
3082
  viewBox: this.viewBox(),
2473
3083
  preserveAspectRatio: "xMidYMid meet",
2474
3084
  onClick: this.onClick.bind(this),
3085
+ onDblClick: this.onDoubleClick.bind(this),
2475
3086
  children: [
2476
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
2477
- Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, false)),
2478
- Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, true))
3087
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
3088
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3089
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
2479
3090
  ] }),
2480
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3091
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2481
3092
  "g",
2482
3093
  {
2483
3094
  ref: (el) => this.group = el,
@@ -2496,14 +3107,34 @@ var Canvas = class {
2496
3107
  this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
2497
3108
  document.addEventListener("mousemove", this.onMouseMove.bind(this));
2498
3109
  document.addEventListener("mouseup", this.onMouseUp.bind(this));
3110
+ document.addEventListener("keydown", this.onKeyDown.bind(this));
2499
3111
  this.createZoomControls();
2500
3112
  }
3113
+ onKeyDown(e) {
3114
+ if (e.key === "Escape" && this.editMode.isCreatingEdge) {
3115
+ this.endNewEdge(true);
3116
+ }
3117
+ }
2501
3118
  /** Convert screen coordinates to canvas-relative coordinates */
2502
3119
  screenToCanvas(screen) {
2503
3120
  const rect = this.container.getBoundingClientRect();
2504
3121
  return canvasPos(screen.x - rect.left, screen.y - rect.top);
2505
3122
  }
2506
- /**
3123
+ /** Convert canvas coordinates to graph coordinates */
3124
+ canvasToGraph(canvas) {
3125
+ const vb = this.currentViewBox();
3126
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
3127
+ return graphPos(
3128
+ vb.x - offsetX + canvas.x * scale,
3129
+ vb.y - offsetY + canvas.y * scale
3130
+ );
3131
+ }
3132
+ /** Convert screen coordinates to graph coordinates */
3133
+ screenToGraph(screen) {
3134
+ const canvas = this.screenToCanvas(screen);
3135
+ return this.canvasToGraph(canvas);
3136
+ }
3137
+ /**
2507
3138
  * Get the effective scale from canvas pixels to graph units,
2508
3139
  * accounting for preserveAspectRatio="xMidYMid meet" which uses
2509
3140
  * the smaller scale (to fit) and centers the content.
@@ -2520,15 +3151,6 @@ var Canvas = class {
2520
3151
  const offsetY = (actualH - vb.h) / 2;
2521
3152
  return { scale, offsetX, offsetY };
2522
3153
  }
2523
- /** Convert canvas coordinates to graph coordinates */
2524
- canvasToGraph(canvas) {
2525
- const vb = this.currentViewBox();
2526
- const { scale, offsetX, offsetY } = this.getEffectiveScale();
2527
- return graphPos(
2528
- vb.x - offsetX + canvas.x * scale,
2529
- vb.y - offsetY + canvas.y * scale
2530
- );
2531
- }
2532
3154
  /** Get current viewBox as an object */
2533
3155
  currentViewBox() {
2534
3156
  const p = this.padding;
@@ -2563,28 +3185,67 @@ var Canvas = class {
2563
3185
  onMouseDown(e) {
2564
3186
  if (e.button !== 0) return;
2565
3187
  if (e.target.closest(".g3p-zoom-controls")) return;
2566
- this.isPanning = true;
2567
- this.panStart = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2568
- this.transformStart = { ...this.transform };
2569
- const { scale } = this.getEffectiveScale();
2570
- this.panScale = { x: scale, y: scale };
2571
- this.container.style.cursor = "grabbing";
2572
- e.preventDefault();
3188
+ const hit = this.hitTest(e.clientX, e.clientY);
3189
+ if (this.editMode.editable && (hit.type === "node" || hit.type === "port")) {
3190
+ const node = hit.node;
3191
+ if (node.isDummy) return;
3192
+ e.preventDefault();
3193
+ e.stopPropagation();
3194
+ const startGraph = this.screenToGraph(hit.center);
3195
+ const portId = hit.type === "port" ? hit.port : void 0;
3196
+ this.pendingDrag = {
3197
+ timeout: window.setTimeout(() => {
3198
+ if (this.pendingDrag) {
3199
+ this.startNewEdge(this.pendingDrag.nodeId, this.pendingDrag.startGraph, this.pendingDrag.portId);
3200
+ this.pendingDrag = null;
3201
+ }
3202
+ }, 200),
3203
+ nodeId: node.data.id,
3204
+ startGraph,
3205
+ portId
3206
+ };
3207
+ return;
3208
+ }
3209
+ if (hit.type === "canvas" || hit.type === "edge") {
3210
+ const startCanvas = this.screenToCanvas(screenPos(e.clientX, e.clientY));
3211
+ this.editMode.startPan(startCanvas, { ...this.transform });
3212
+ const { scale } = this.getEffectiveScale();
3213
+ this.panScale = { x: scale, y: scale };
3214
+ this.container.style.cursor = "grabbing";
3215
+ e.preventDefault();
3216
+ }
2573
3217
  }
2574
3218
  onMouseMove(e) {
2575
- if (!this.isPanning || !this.panStart || !this.transformStart || !this.panScale) return;
3219
+ if (this.editMode.isCreatingEdge) {
3220
+ const screenCursor = screenPos(e.clientX, e.clientY);
3221
+ const canvasCursor = this.screenToCanvas(screenCursor);
3222
+ const graphCursor = this.canvasToGraph(canvasCursor);
3223
+ this.editMode.updateNewEdgePosition(graphCursor);
3224
+ this.updateNewEdgeVisual();
3225
+ this.detectHoverTarget(e.clientX, e.clientY);
3226
+ return;
3227
+ }
3228
+ if (!this.editMode.isPanning || !this.panScale) return;
3229
+ const panState = this.editMode.state;
3230
+ if (panState.type !== "panning") return;
2576
3231
  const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2577
- const dx = current.x - this.panStart.x;
2578
- const dy = current.y - this.panStart.y;
2579
- this.transform.x = this.transformStart.x + dx * this.panScale.x;
2580
- this.transform.y = this.transformStart.y + dy * this.panScale.y;
3232
+ const dx = current.x - panState.startCanvas.x;
3233
+ const dy = current.y - panState.startCanvas.y;
3234
+ this.transform.x = panState.startTransform.x + dx * this.panScale.x;
3235
+ this.transform.y = panState.startTransform.y + dy * this.panScale.y;
2581
3236
  this.applyTransform();
2582
3237
  }
2583
3238
  onMouseUp(e) {
2584
- if (!this.isPanning) return;
2585
- this.isPanning = false;
2586
- this.panStart = null;
2587
- this.transformStart = null;
3239
+ if (this.pendingDrag) {
3240
+ window.clearTimeout(this.pendingDrag.timeout);
3241
+ this.pendingDrag = null;
3242
+ }
3243
+ if (this.editMode.isCreatingEdge) {
3244
+ this.endNewEdge(false);
3245
+ return;
3246
+ }
3247
+ if (!this.editMode.isPanning) return;
3248
+ this.editMode.reset();
2588
3249
  this.panScale = null;
2589
3250
  this.container.style.cursor = "";
2590
3251
  }
@@ -2594,12 +3255,11 @@ var Canvas = class {
2594
3255
  this.updateZoomLevel();
2595
3256
  }
2596
3257
  createZoomControls() {
2597
- const c = styler("zoom", import_zoom.default, this.classPrefix);
2598
- this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: c("controls"), children: [
2599
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomIn(), children: "+" }),
2600
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: c("level"), id: "g3p-zoom-level", children: "100%" }),
2601
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomOut(), children: "\u2212" }),
2602
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
3258
+ this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "g3p-zoom-controls", children: [
3259
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomIn(), children: "+" }),
3260
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "g3p-zoom-level", id: "g3p-zoom-level", children: "100%" }),
3261
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomOut(), children: "\u2212" }),
3262
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn g3p-zoom-reset", onClick: () => this.zoomReset(), children: "\u27F2" })
2603
3263
  ] });
2604
3264
  this.container.appendChild(this.zoomControls);
2605
3265
  }
@@ -2621,17 +3281,215 @@ var Canvas = class {
2621
3281
  this.transform = { x: 0, y: 0, scale: 1 };
2622
3282
  this.applyTransform();
2623
3283
  }
3284
+ // ==================== New-Edge Mode ====================
3285
+ /** Start creating a new edge from a node */
3286
+ startNewEdge(sourceNodeId, startGraph, sourcePortId) {
3287
+ this.editMode.startNewEdge(sourceNodeId, startGraph, sourcePortId);
3288
+ this.updateNewEdgeVisual();
3289
+ this.container.style.cursor = "crosshair";
3290
+ }
3291
+ /** Update the new-edge visual during drag */
3292
+ updateNewEdgeVisual() {
3293
+ const state = this.editMode.getNewEdgeState();
3294
+ if (!state) {
3295
+ this.removeNewEdgeVisual();
3296
+ return;
3297
+ }
3298
+ if (this.newEdgeEl) {
3299
+ this.newEdgeEl.remove();
3300
+ }
3301
+ this.newEdgeEl = renderNewEdge({
3302
+ start: state.startGraph,
3303
+ end: state.currentGraph
3304
+ });
3305
+ this.group.appendChild(this.newEdgeEl);
3306
+ }
3307
+ /** Remove the new-edge visual */
3308
+ removeNewEdgeVisual() {
3309
+ if (this.newEdgeEl) {
3310
+ this.newEdgeEl.remove();
3311
+ this.newEdgeEl = void 0;
3312
+ }
3313
+ }
3314
+ /** Complete or cancel the new-edge creation */
3315
+ endNewEdge(cancelled = false) {
3316
+ const state = this.editMode.getNewEdgeState();
3317
+ if (!state) return;
3318
+ if (!cancelled) {
3319
+ const { target, source } = state;
3320
+ if (target?.type == "node") {
3321
+ this.api.handleAddEdge({ id: "", source, target });
3322
+ } else {
3323
+ this.api.handleNewNodeFrom(source);
3324
+ }
3325
+ }
3326
+ this.removeNewEdgeVisual();
3327
+ this.clearDropTargetHighlight();
3328
+ this.editMode.reset();
3329
+ this.container.style.cursor = "";
3330
+ }
3331
+ /** Find node data by internal ID */
3332
+ findNodeDataById(nodeId) {
3333
+ for (const node of this.curNodes.values()) {
3334
+ if (node.data?.id === nodeId) {
3335
+ return node.data.data;
3336
+ }
3337
+ }
3338
+ return null;
3339
+ }
3340
+ /** Set hover target for new-edge mode */
3341
+ setNewEdgeHoverTarget(id, port) {
3342
+ this.clearDropTargetHighlight();
3343
+ this.editMode.setHoverTarget({ type: "node", id, port });
3344
+ if (port) {
3345
+ const portEl = this.container?.querySelector(`.g3p-node-port[data-node-id="${id}"][data-port-id="${port}"]`);
3346
+ if (portEl) portEl.classList.add("g3p-drop-target");
3347
+ } else {
3348
+ const node = this.curNodes.get(id);
3349
+ if (node?.container) node.container.classList.add("g3p-drop-target");
3350
+ }
3351
+ }
3352
+ /** Clear hover target for new-edge mode */
3353
+ clearNewEdgeHoverTarget() {
3354
+ this.clearDropTargetHighlight();
3355
+ this.editMode.setHoverTarget(null);
3356
+ }
3357
+ /** Remove drop target highlight from all elements */
3358
+ clearDropTargetHighlight() {
3359
+ for (const node of this.curNodes.values()) {
3360
+ node.container?.classList.remove("g3p-drop-target");
3361
+ }
3362
+ this.container?.querySelectorAll(".g3p-drop-target").forEach((el) => {
3363
+ el.classList.remove("g3p-drop-target");
3364
+ });
3365
+ }
3366
+ /** Detect hover target during new-edge drag using elementFromPoint */
3367
+ detectHoverTarget(clientX, clientY) {
3368
+ if (this.newEdgeEl) {
3369
+ this.newEdgeEl.style.display = "none";
3370
+ }
3371
+ const el = document.elementFromPoint(clientX, clientY);
3372
+ if (this.newEdgeEl) {
3373
+ this.newEdgeEl.style.display = "";
3374
+ }
3375
+ if (!el) {
3376
+ this.clearNewEdgeHoverTarget();
3377
+ return;
3378
+ }
3379
+ const portEl = el.closest(".g3p-node-port");
3380
+ if (portEl) {
3381
+ const nodeId = portEl.getAttribute("data-node-id");
3382
+ const portId = portEl.getAttribute("data-port-id");
3383
+ if (nodeId && portId) {
3384
+ const node = this.curNodes.get(nodeId);
3385
+ if (node && !node.isDummy) {
3386
+ this.setNewEdgeHoverTarget(nodeId, portId);
3387
+ return;
3388
+ }
3389
+ }
3390
+ }
3391
+ const nodeEl = el.closest(".g3p-node-container");
3392
+ if (nodeEl) {
3393
+ const nodeId = nodeEl.getAttribute("data-node-id");
3394
+ if (nodeId) {
3395
+ const node = this.curNodes.get(nodeId);
3396
+ if (node && !node.isDummy) {
3397
+ this.setNewEdgeHoverTarget(node.data.id);
3398
+ return;
3399
+ }
3400
+ }
3401
+ }
3402
+ this.clearNewEdgeHoverTarget();
3403
+ }
3404
+ // ==================== Hit Testing ====================
3405
+ /** Result of a hit test */
3406
+ hitTest(clientX, clientY) {
3407
+ const el = document.elementFromPoint(clientX, clientY);
3408
+ if (!el) return { type: "canvas" };
3409
+ const getCenter = (el2) => {
3410
+ const rect = el2.getBoundingClientRect();
3411
+ return screenPos(rect.left + rect.width / 2, rect.top + rect.height / 2);
3412
+ };
3413
+ const portEl = el.closest(".g3p-node-port");
3414
+ if (portEl) {
3415
+ const nodeId = portEl.getAttribute("data-node-id");
3416
+ const portId = portEl.getAttribute("data-port-id");
3417
+ if (nodeId && portId) {
3418
+ const center = getCenter(portEl);
3419
+ const node = this.curNodes.get(nodeId);
3420
+ if (node) {
3421
+ return { type: "port", node, port: portId, center };
3422
+ }
3423
+ }
3424
+ }
3425
+ const nodeEl = el.closest(".g3p-node-container");
3426
+ if (nodeEl) {
3427
+ const nodeId = nodeEl.getAttribute("data-node-id");
3428
+ if (nodeId) {
3429
+ const borderEl = el.closest(".g3p-node-border");
3430
+ const center = getCenter(borderEl ?? nodeEl);
3431
+ const node = this.curNodes.get(nodeId);
3432
+ if (node) {
3433
+ return { type: "node", node, center };
3434
+ }
3435
+ }
3436
+ }
3437
+ const edgeEl = el.closest(".g3p-seg-container");
3438
+ if (edgeEl) {
3439
+ const segId = edgeEl.getAttribute("data-edge-id");
3440
+ if (segId) {
3441
+ return { type: "edge", segId };
3442
+ }
3443
+ }
3444
+ return { type: "canvas" };
3445
+ }
2624
3446
  };
3447
+ var themeVarMap = {
3448
+ // Canvas
3449
+ bg: "--g3p-bg",
3450
+ shadow: "--g3p-shadow",
3451
+ // Node
3452
+ border: "--g3p-border",
3453
+ borderHover: "--g3p-border-hover",
3454
+ borderSelected: "--g3p-border-selected",
3455
+ text: "--g3p-text",
3456
+ textMuted: "--g3p-text-muted",
3457
+ // Port
3458
+ bgHover: "--g3p-port-bg-hover",
3459
+ // Edge
3460
+ color: "--g3p-edge-color"
3461
+ };
3462
+ function themeToCSS(theme, selector, prefix = "") {
3463
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
3464
+ if (!entries.length) return "";
3465
+ let css = `${selector} {
3466
+ `;
3467
+ for (const [key, value] of entries) {
3468
+ let cssVar = themeVarMap[key];
3469
+ if (key === "bg" && prefix === "node") {
3470
+ cssVar = "--g3p-bg-node";
3471
+ } else if (key === "bg" && prefix === "port") {
3472
+ cssVar = "--g3p-port-bg";
3473
+ }
3474
+ if (cssVar) {
3475
+ css += ` ${cssVar}: ${value};
3476
+ `;
3477
+ }
3478
+ }
3479
+ css += "}\n";
3480
+ return css;
3481
+ }
2625
3482
 
2626
3483
  // src/canvas/render-node.tsx
2627
- var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2628
- function renderNode(node) {
3484
+ var import_jsx_runtime7 = require("jsx-dom/jsx-runtime");
3485
+ function renderNode(node, props) {
2629
3486
  if (typeof node == "string") node = { id: node };
2630
- const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
3487
+ const title = node?.title ?? props?.title ?? node?.label ?? node?.name ?? node?.text ?? props?.text ?? node?.id ?? "?";
2631
3488
  const detail = node?.detail ?? node?.description ?? node?.subtitle;
2632
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-node-default", children: [
2633
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-title", children: title }),
2634
- detail && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-detail", children: detail })
3489
+ console.log(`renderNode: ${node.id} ${title} ${detail}`);
3490
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "g3p-node-default", children: [
3491
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-title", children: title }),
3492
+ detail && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-detail", children: detail })
2635
3493
  ] });
2636
3494
  }
2637
3495
 
@@ -2663,7 +3521,6 @@ function defaults() {
2663
3521
  },
2664
3522
  canvas: {
2665
3523
  renderNode,
2666
- classPrefix: "g3p",
2667
3524
  width: "100%",
2668
3525
  height: "100%",
2669
3526
  padding: 20,
@@ -2740,22 +3597,30 @@ var API = class {
2740
3597
  nodeIds;
2741
3598
  edgeIds;
2742
3599
  nodeVersions;
3600
+ nodeOverrides;
3601
+ edgeOverrides;
3602
+ nodeFields;
2743
3603
  nextNodeId;
2744
3604
  nextEdgeId;
3605
+ events;
2745
3606
  root;
2746
3607
  constructor(args) {
2747
3608
  this.root = args.root;
2748
3609
  this.options = applyDefaults(args.options);
2749
3610
  let graph2 = new Graph({ options: this.options.graph });
2750
3611
  this.state = { graph: graph2, update: null };
3612
+ this.events = args.events || {};
2751
3613
  this.seq = [this.state];
2752
3614
  this.index = 0;
2753
3615
  this.nodeIds = /* @__PURE__ */ new Map();
2754
3616
  this.edgeIds = /* @__PURE__ */ new Map();
2755
3617
  this.nodeVersions = /* @__PURE__ */ new Map();
3618
+ this.nodeOverrides = /* @__PURE__ */ new Map();
3619
+ this.edgeOverrides = /* @__PURE__ */ new Map();
3620
+ this.nodeFields = /* @__PURE__ */ new Map();
2756
3621
  this.nextNodeId = 1;
2757
3622
  this.nextEdgeId = 1;
2758
- this.canvas = new Canvas({
3623
+ this.canvas = new Canvas(this, {
2759
3624
  ...this.options.canvas,
2760
3625
  dummyNodeSize: this.options.graph.dummyNodeSize,
2761
3626
  orientation: this.options.graph.orientation
@@ -2768,6 +3633,22 @@ var API = class {
2768
3633
  this.history = [];
2769
3634
  }
2770
3635
  }
3636
+ /** Current history index (0-based) */
3637
+ getHistoryIndex() {
3638
+ return this.index;
3639
+ }
3640
+ /** Current history length */
3641
+ getHistoryLength() {
3642
+ return this.seq.length;
3643
+ }
3644
+ /** Toggle canvas editable mode without re-creating the graph */
3645
+ setEditable(editable) {
3646
+ this.canvas.editMode.editable = editable;
3647
+ }
3648
+ get graph() {
3649
+ return this.state.graph;
3650
+ }
3651
+ /** Initialize the API */
2771
3652
  async init() {
2772
3653
  const root = document.getElementById(this.root);
2773
3654
  if (!root) throw new Error("root element not found");
@@ -2775,6 +3656,7 @@ var API = class {
2775
3656
  for (const update of this.history)
2776
3657
  await this.applyUpdate(update);
2777
3658
  }
3659
+ /** Navigate to a different state */
2778
3660
  nav(nav) {
2779
3661
  let newIndex;
2780
3662
  switch (nav) {
@@ -2796,6 +3678,8 @@ var API = class {
2796
3678
  this.applyDiff(this.index, newIndex);
2797
3679
  this.index = newIndex;
2798
3680
  this.state = this.seq[this.index];
3681
+ if (this.events.historyChange)
3682
+ this.events.historyChange(this.index, this.seq.length);
2799
3683
  }
2800
3684
  applyDiff(oldIndex, newIndex) {
2801
3685
  const oldGraph = this.seq[oldIndex].graph;
@@ -2805,36 +3689,72 @@ var API = class {
2805
3689
  if (!newNode) this.canvas.deleteNode(oldNode);
2806
3690
  }
2807
3691
  for (const newNode of newGraph.nodes.values()) {
2808
- if (!oldGraph.nodes.has(newNode.id)) this.canvas.addNode(newNode);
3692
+ const oldNode = oldGraph.nodes.get(newNode.id);
3693
+ if (!oldNode) {
3694
+ this.canvas.addNode(newNode);
3695
+ } else if (oldNode.key !== newNode.key) {
3696
+ this.canvas.deleteNode(oldNode);
3697
+ this.canvas.addNode(newNode);
3698
+ } else if (oldNode.pos !== newNode.pos) {
3699
+ this.canvas.getNode(newNode.key).setPos(newNode.pos);
3700
+ }
2809
3701
  }
2810
3702
  for (const oldSeg of oldGraph.segs.values()) {
2811
3703
  const newSeg = newGraph.segs.get(oldSeg.id);
2812
- if (!newSeg)
3704
+ if (!newSeg) {
2813
3705
  this.canvas.deleteSeg(oldSeg);
2814
- else if (oldSeg.svg != newSeg.svg)
2815
- this.canvas.updateSeg(newSeg);
3706
+ } else if (oldSeg !== newSeg) {
3707
+ this.canvas.updateSeg(newSeg, newGraph);
3708
+ }
2816
3709
  }
2817
3710
  for (const newSeg of newGraph.segs.values()) {
2818
- if (!oldGraph.segs.has(newSeg.id))
3711
+ if (!oldGraph.segs.has(newSeg.id)) {
2819
3712
  this.canvas.addSeg(newSeg, newGraph);
3713
+ }
2820
3714
  }
2821
3715
  this.canvas.update();
2822
3716
  }
3717
+ /** Add a node */
2823
3718
  async addNode(node) {
2824
3719
  await this.update((update) => update.addNode(node));
2825
3720
  }
3721
+ /** Delete a node */
2826
3722
  async deleteNode(node) {
2827
3723
  await this.update((update) => update.deleteNode(node));
2828
3724
  }
3725
+ /** Update a node */
2829
3726
  async updateNode(node) {
2830
3727
  await this.update((update) => update.updateNode(node));
2831
3728
  }
3729
+ /** Add an edge */
2832
3730
  async addEdge(edge) {
2833
3731
  await this.update((update) => update.addEdge(edge));
2834
3732
  }
3733
+ /** Delete an edge */
2835
3734
  async deleteEdge(edge) {
2836
3735
  await this.update((update) => update.deleteEdge(edge));
2837
3736
  }
3737
+ /** Update an edge */
3738
+ async updateEdge(edge) {
3739
+ await this.update((update) => update.updateEdge(edge));
3740
+ }
3741
+ /** Perform a batch of updates */
3742
+ async update(callback) {
3743
+ const updater = new Updater();
3744
+ callback(updater);
3745
+ await this.applyUpdate(updater.update);
3746
+ }
3747
+ /** Rebuild the graph from scratch (removes all then re-adds all nodes/edges) */
3748
+ async rebuild() {
3749
+ const nodes = [...this.nodeIds.keys()];
3750
+ const edges = [...this.edgeIds.keys()];
3751
+ await this.update((updater) => {
3752
+ for (const edge of edges) updater.deleteEdge(edge);
3753
+ for (const node of nodes) updater.deleteNode(node);
3754
+ for (const node of nodes) updater.addNode(node);
3755
+ for (const edge of edges) updater.addEdge(edge);
3756
+ });
3757
+ }
2838
3758
  async applyUpdate(update) {
2839
3759
  log11.info("applyUpdate", update);
2840
3760
  const nodes = await this.measureNodes(update);
@@ -2851,12 +3771,16 @@ var API = class {
2851
3771
  this._addEdge(edge, mut);
2852
3772
  for (const edge of update.updateEdges ?? [])
2853
3773
  this._updateEdge(edge, mut);
3774
+ this.nodeOverrides.clear();
3775
+ this.edgeOverrides.clear();
2854
3776
  });
2855
3777
  this.state = { graph: graph2, update };
2856
3778
  this.setNodePositions();
2857
3779
  this.seq.splice(this.index + 1);
2858
3780
  this.seq.push(this.state);
2859
3781
  this.nav("last");
3782
+ if (this.events.historyChange)
3783
+ this.events.historyChange(this.index, this.seq.length);
2860
3784
  }
2861
3785
  setNodePositions() {
2862
3786
  const { graph: graph2 } = this.state;
@@ -2866,11 +3790,6 @@ var API = class {
2866
3790
  this.canvas.getNode(node.key).setPos(node.pos);
2867
3791
  }
2868
3792
  }
2869
- async update(callback) {
2870
- const updater = new Updater();
2871
- callback(updater);
2872
- await this.applyUpdate(updater.update);
2873
- }
2874
3793
  async measureNodes(update) {
2875
3794
  const data = [];
2876
3795
  for (const set of [update.updateNodes, update.addNodes])
@@ -2885,7 +3804,10 @@ var API = class {
2885
3804
  else if (!data) throw new Error(`invalid node ${data}`);
2886
3805
  else if (typeof data == "string") props = { id: data };
2887
3806
  else if (typeof data == "object") props = data;
2888
- else throw new Error(`invalid node ${data}`);
3807
+ else throw new Error(`invalid node ${JSON.stringify(data)}`);
3808
+ this.detectNodeFields(data);
3809
+ const overrides = this.nodeOverrides.get(data);
3810
+ if (overrides) props = { ...props, ...overrides };
2889
3811
  let { id, title, text, type, render } = props;
2890
3812
  id ??= this.getNodeId(data);
2891
3813
  const ports = this.parsePorts(props.ports);
@@ -2895,6 +3817,21 @@ var API = class {
2895
3817
  this.nodeVersions.set(data, version);
2896
3818
  return { id, data, ports, title, text, type, render, version };
2897
3819
  }
3820
+ detectNodeFields(data) {
3821
+ if (typeof data != "object" || !data) return;
3822
+ const skip = /* @__PURE__ */ new Set(["id", "ports", "render", "version"]);
3823
+ for (const [key, value] of Object.entries(data)) {
3824
+ if (skip.has(key)) continue;
3825
+ if (value === null || value === void 0) continue;
3826
+ const type = typeof value;
3827
+ if (type === "string" || type === "number" || type === "boolean") {
3828
+ this.nodeFields.set(key, type);
3829
+ }
3830
+ }
3831
+ }
3832
+ getNodeFields() {
3833
+ return this.nodeFields;
3834
+ }
2898
3835
  parseEdge(data) {
2899
3836
  const get = this.options.props.edge;
2900
3837
  let props;
@@ -2903,8 +3840,10 @@ var API = class {
2903
3840
  else if (typeof data == "string") props = this.parseStringEdge(data);
2904
3841
  else if (typeof data == "object") props = data;
2905
3842
  else throw new Error(`invalid edge ${data}`);
3843
+ const overrides = this.edgeOverrides.get(data);
3844
+ if (overrides) props = { ...props, ...overrides };
2906
3845
  let { id, source, target, type } = props;
2907
- id ??= this.getEdgeId(data);
3846
+ if (!id) id = this.getEdgeId(data);
2908
3847
  source = this.parseEdgeEnd(source);
2909
3848
  target = this.parseEdgeEnd(target);
2910
3849
  const edge = { id, source, target, type, data };
@@ -2917,14 +3856,14 @@ var API = class {
2917
3856
  const keys = Object.keys(end);
2918
3857
  const pidx = keys.indexOf("port");
2919
3858
  if (pidx != -1) {
2920
- if (typeof end.port != "string") return end;
3859
+ if (end.port !== void 0 && typeof end.port != "string") return end;
2921
3860
  keys.splice(pidx, 1);
2922
3861
  }
2923
3862
  if (keys.length != 1) return end;
2924
3863
  if (keys[0] == "id") return end;
2925
3864
  if (keys[0] != "node") return end;
2926
3865
  const id = this.nodeIds.get(end.node);
2927
- if (!id) throw new Error(`edge end ${end} references unknown node ${end.node}`);
3866
+ if (!id) throw new Error(`edge end references unknown node ${end.node}`);
2928
3867
  return { id, port: end.port };
2929
3868
  }
2930
3869
  throw new Error(`invalid edge end ${end}`);
@@ -2934,13 +3873,19 @@ var API = class {
2934
3873
  return { source, target };
2935
3874
  }
2936
3875
  parsePorts(ports) {
2937
- const fixed = { in: null, out: null };
3876
+ const fixed = {};
2938
3877
  for (const key of ["in", "out"]) {
2939
3878
  if (ports?.[key] && ports[key].length > 0)
2940
3879
  fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
2941
3880
  }
2942
3881
  return fixed;
2943
3882
  }
3883
+ getNode(id) {
3884
+ return this.graph.getNode(id);
3885
+ }
3886
+ getEdge(id) {
3887
+ return this.graph.getEdge(id);
3888
+ }
2944
3889
  getNodeId(node) {
2945
3890
  let id = this.nodeIds.get(node);
2946
3891
  if (!id) {
@@ -2960,7 +3905,6 @@ var API = class {
2960
3905
  _addNode(node, mut) {
2961
3906
  const { data, id: newId } = node.data;
2962
3907
  const oldId = this.nodeIds.get(data);
2963
- console.log("addNode", node, oldId, newId);
2964
3908
  if (oldId && oldId != newId)
2965
3909
  throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
2966
3910
  this.nodeIds.set(data, newId);
@@ -2968,36 +3912,169 @@ var API = class {
2968
3912
  }
2969
3913
  _removeNode(node, mut) {
2970
3914
  const id = this.nodeIds.get(node);
2971
- if (!id) throw new Error(`removing node ${node} which does not exist`);
3915
+ if (!id) throw new Error(`removing node ${JSON.stringify(node)} which does not exist`);
2972
3916
  mut.removeNode({ id });
2973
3917
  }
2974
3918
  _updateNode(node, mut) {
2975
3919
  const { data, id: newId } = node.data;
2976
3920
  const oldId = this.nodeIds.get(data);
2977
- if (!oldId) throw new Error(`updating unknown node ${node}`);
2978
- if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
3921
+ if (!oldId) throw new Error(`updating unknown node ${JSON.stringify(node)} `);
3922
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId} `);
2979
3923
  mut.updateNode(node.data);
2980
3924
  }
2981
3925
  _addEdge(edge, mut) {
2982
3926
  const data = this.parseEdge(edge);
2983
3927
  const id = this.edgeIds.get(edge);
2984
3928
  if (id && id != data.id)
2985
- throw new Error(`edge id changed from ${id} to ${data.id}`);
3929
+ throw new Error(`edge id changed from ${id} to ${data.id} `);
2986
3930
  this.edgeIds.set(edge, data.id);
2987
3931
  mut.addEdge(data);
2988
3932
  }
2989
3933
  _removeEdge(edge, mut) {
2990
3934
  const id = this.edgeIds.get(edge);
2991
- if (!id) throw new Error(`removing edge ${edge} which does not exist`);
3935
+ if (!id) throw new Error(`removing edge ${JSON.stringify(edge)} which does not exist`);
2992
3936
  mut.removeEdge(this.parseEdge(edge));
2993
3937
  }
2994
3938
  _updateEdge(edge, mut) {
2995
3939
  const id = this.edgeIds.get(edge);
2996
- if (!id) throw new Error(`updating unknown edge ${edge}`);
3940
+ if (!id) throw new Error(`updating unknown edge ${JSON.stringify(edge)} `);
2997
3941
  const data = this.parseEdge(edge);
2998
- if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
3942
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id} `);
2999
3943
  mut.updateEdge(data);
3000
3944
  }
3945
+ // Event Handlers
3946
+ handleClickNode(id) {
3947
+ const handler = this.events.nodeClick;
3948
+ const node = this.graph.getNode(id);
3949
+ if (handler) handler(node.data);
3950
+ }
3951
+ handleClickEdge(id) {
3952
+ const handler = this.events.edgeClick;
3953
+ if (!handler) return;
3954
+ const seg = this.graph.getSeg(id);
3955
+ if (seg.edgeIds.size != 1) return;
3956
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
3957
+ handler(edge.data);
3958
+ }
3959
+ async handleNewNode() {
3960
+ const gotNode = async (node) => {
3961
+ await this.addNode(node);
3962
+ };
3963
+ if (this.events.newNode)
3964
+ this.events.newNode(gotNode);
3965
+ else
3966
+ this.canvas.showNewNodeModal(async (data) => {
3967
+ if (this.events.addNode)
3968
+ this.events.addNode(data, gotNode);
3969
+ else
3970
+ await gotNode(data);
3971
+ });
3972
+ }
3973
+ async handleNewNodeFrom(source) {
3974
+ const gotNode = async (node) => {
3975
+ const gotEdge = async (edge) => {
3976
+ await this.update((u) => {
3977
+ u.addNode(node).addEdge(edge);
3978
+ });
3979
+ };
3980
+ const data = this.graph.getNode(source.id).data;
3981
+ const newEdge = {
3982
+ source: { node: data, port: source.port },
3983
+ target: { node }
3984
+ };
3985
+ if (this.events.addEdge)
3986
+ this.events.addEdge(newEdge, gotEdge);
3987
+ else
3988
+ await gotEdge(newEdge);
3989
+ };
3990
+ if (this.events.newNode)
3991
+ this.events.newNode(gotNode);
3992
+ else
3993
+ this.canvas.showNewNodeModal(async (data) => {
3994
+ if (this.events.addNode)
3995
+ this.events.addNode(data, gotNode);
3996
+ else
3997
+ await gotNode(data);
3998
+ });
3999
+ }
4000
+ async handleEditNode(id) {
4001
+ const node = this.graph.getNode(id);
4002
+ const gotNode = async (node2) => {
4003
+ if (node2) await this.updateNode(node2);
4004
+ };
4005
+ if (this.events.editNode)
4006
+ this.events.editNode(node.data, gotNode);
4007
+ else {
4008
+ this.canvas.showEditNodeModal(node, async (data) => {
4009
+ if (this.events.updateNode)
4010
+ this.events.updateNode(node.data, data, gotNode);
4011
+ else {
4012
+ this.nodeOverrides.set(node.data, data);
4013
+ await gotNode(node.data);
4014
+ }
4015
+ });
4016
+ }
4017
+ }
4018
+ async handleEditEdge(id) {
4019
+ const seg = this.graph.getSeg(id);
4020
+ if (seg.edgeIds.size != 1) return;
4021
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
4022
+ const gotEdge = async (edge2) => {
4023
+ if (edge2) await this.updateEdge(edge2);
4024
+ };
4025
+ if (this.events.editEdge)
4026
+ this.events.editEdge(edge.data, gotEdge);
4027
+ else
4028
+ this.canvas.showEditEdgeModal(edge, async (data) => {
4029
+ const sourceNode = edge.sourceNode(this.graph);
4030
+ const targetNode = edge.targetNode(this.graph);
4031
+ const update = {
4032
+ source: { node: sourceNode.data, port: data.source.port, marker: data.source.marker },
4033
+ target: { node: targetNode.data, port: data.target.port, marker: data.target.marker }
4034
+ };
4035
+ if (this.events.updateEdge)
4036
+ this.events.updateEdge(edge.data, update, gotEdge);
4037
+ else {
4038
+ this.edgeOverrides.set(edge.data, {
4039
+ source: { id: sourceNode.id, port: data.source.port, marker: data.source.marker },
4040
+ target: { id: targetNode.id, port: data.target.port, marker: data.target.marker },
4041
+ type: data.type
4042
+ });
4043
+ await gotEdge(edge.data);
4044
+ }
4045
+ });
4046
+ }
4047
+ async handleAddEdge(data) {
4048
+ const gotEdge = async (edge) => {
4049
+ if (edge) await this.addEdge(edge);
4050
+ };
4051
+ const newEdge = {
4052
+ source: { node: this.graph.getNode(data.source.id).data, port: data.source.port, marker: data.source.marker },
4053
+ target: { node: this.graph.getNode(data.target.id).data, port: data.target.port, marker: data.target.marker }
4054
+ };
4055
+ if (this.events.addEdge)
4056
+ this.events.addEdge(newEdge, gotEdge);
4057
+ else
4058
+ await gotEdge(data);
4059
+ }
4060
+ async handleDeleteNode(id) {
4061
+ const node = this.getNode(id);
4062
+ if (this.events.removeNode)
4063
+ this.events.removeNode(node.data, async (remove) => {
4064
+ if (remove) await this.deleteNode(node.data);
4065
+ });
4066
+ else
4067
+ await this.deleteNode(node.data);
4068
+ }
4069
+ async handleDeleteEdge(id) {
4070
+ const edge = this.getEdge(id);
4071
+ if (this.events.removeEdge)
4072
+ this.events.removeEdge(edge.data, async (remove) => {
4073
+ if (remove) await this.deleteEdge(edge.data);
4074
+ });
4075
+ else
4076
+ await this.deleteEdge(edge.data);
4077
+ }
3001
4078
  };
3002
4079
 
3003
4080
  // src/index.ts