@3plate/graph-core 0.1.4 → 0.1.6
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 +2428 -312
- package/dist/index.d.cts +449 -16
- package/dist/index.d.ts +449 -16
- package/dist/index.js +2424 -313
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
FileSource: () => FileSource,
|
|
34
|
+
FileSystemSource: () => FileSystemSource,
|
|
35
|
+
Ingest: () => Ingest,
|
|
36
|
+
Playground: () => Playground,
|
|
37
|
+
WebSocketSource: () => WebSocketSource,
|
|
33
38
|
default: () => index_default,
|
|
34
39
|
graph: () => graph
|
|
35
40
|
});
|
|
@@ -40,6 +45,70 @@ var import_immutable8 = require("immutable");
|
|
|
40
45
|
|
|
41
46
|
// src/graph/node.ts
|
|
42
47
|
var import_immutable = require("immutable");
|
|
48
|
+
|
|
49
|
+
// src/log.ts
|
|
50
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
51
|
+
var resolvedLevel = typeof globalThis !== "undefined" && globalThis.__LOG_LEVEL || typeof process !== "undefined" && process.env?.LOG_LEVEL || "debug";
|
|
52
|
+
var browserOpts = { asObject: true };
|
|
53
|
+
browserOpts.transmit = {
|
|
54
|
+
level: resolvedLevel,
|
|
55
|
+
send: (level, log12) => {
|
|
56
|
+
try {
|
|
57
|
+
const endpoint = typeof globalThis !== "undefined" && globalThis.__LOG_INGEST_URL || typeof process !== "undefined" && process.env?.LOG_INGEST_URL || void 0;
|
|
58
|
+
if (!endpoint || typeof window === "undefined") {
|
|
59
|
+
try {
|
|
60
|
+
console.debug("[graph-core] transmit skipped", { endpoint, hasWindow: typeof window !== "undefined", level });
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const line = JSON.stringify({ level, ...log12, ts: Date.now() }) + "\n";
|
|
66
|
+
try {
|
|
67
|
+
console.debug("[graph-core] transmit sending", { endpoint, level, bytes: line.length });
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
fetch(endpoint, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
mode: "no-cors",
|
|
73
|
+
credentials: "omit",
|
|
74
|
+
body: line,
|
|
75
|
+
keepalive: true
|
|
76
|
+
}).then(() => {
|
|
77
|
+
try {
|
|
78
|
+
console.debug("[graph-core] transmit fetch ok");
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}).catch((err) => {
|
|
82
|
+
try {
|
|
83
|
+
console.debug("[graph-core] transmit fetch error", err?.message || err);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} catch (e) {
|
|
88
|
+
try {
|
|
89
|
+
console.debug("[graph-core] transmit error", e?.message || e);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var base = (0, import_pino.default)({
|
|
96
|
+
level: resolvedLevel,
|
|
97
|
+
browser: browserOpts
|
|
98
|
+
});
|
|
99
|
+
function logger(module2) {
|
|
100
|
+
const child = base.child({ module: module2 });
|
|
101
|
+
return {
|
|
102
|
+
error: (msg, ...args) => child.error({ args }, msg),
|
|
103
|
+
warn: (msg, ...args) => child.warn({ args }, msg),
|
|
104
|
+
info: (msg, ...args) => child.info({ args }, msg),
|
|
105
|
+
debug: (msg, ...args) => child.debug({ args }, msg)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
var log = logger("core");
|
|
109
|
+
|
|
110
|
+
// src/graph/node.ts
|
|
111
|
+
var log2 = logger("node");
|
|
43
112
|
var defNodeData = {
|
|
44
113
|
id: "",
|
|
45
114
|
data: void 0,
|
|
@@ -48,7 +117,7 @@ var defNodeData = {
|
|
|
48
117
|
text: void 0,
|
|
49
118
|
type: void 0,
|
|
50
119
|
render: void 0,
|
|
51
|
-
ports: {
|
|
120
|
+
ports: {},
|
|
52
121
|
aligned: {},
|
|
53
122
|
edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
|
|
54
123
|
segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
|
|
@@ -82,10 +151,7 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
|
|
|
82
151
|
return this.isDummy ? this.id : _Node.key(this);
|
|
83
152
|
}
|
|
84
153
|
static key(node) {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
static isDummyId(nodeId) {
|
|
88
|
-
return nodeId.startsWith(_Node.dummyPrefix);
|
|
154
|
+
return `k:${node.id}:${node.version}`;
|
|
89
155
|
}
|
|
90
156
|
static addNormal(g, data) {
|
|
91
157
|
const layer = g.layerAt(0);
|
|
@@ -95,7 +161,9 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
|
|
|
95
161
|
segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
|
|
96
162
|
aligned: {},
|
|
97
163
|
edgeIds: [],
|
|
98
|
-
layerId: layer.id
|
|
164
|
+
layerId: layer.id,
|
|
165
|
+
lpos: void 0,
|
|
166
|
+
pos: void 0
|
|
99
167
|
});
|
|
100
168
|
layer.addNode(g, node.id);
|
|
101
169
|
g.nodes.set(node.id, node);
|
|
@@ -158,6 +226,18 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
|
|
|
158
226
|
getLayer(g) {
|
|
159
227
|
return g.getLayer(this.layerId);
|
|
160
228
|
}
|
|
229
|
+
margin(g) {
|
|
230
|
+
return this.isDummy ? g.options.edgeSpacing - g.options.dummyNodeSize : g.options.nodeMargin;
|
|
231
|
+
}
|
|
232
|
+
marginWith(g, other) {
|
|
233
|
+
return Math.max(this.margin(g), other.margin(g));
|
|
234
|
+
}
|
|
235
|
+
width(g) {
|
|
236
|
+
return this.dims?.[g.w] ?? 0;
|
|
237
|
+
}
|
|
238
|
+
right(g) {
|
|
239
|
+
return this.lpos + this.width(g);
|
|
240
|
+
}
|
|
161
241
|
setIndex(g, index) {
|
|
162
242
|
if (this.index == index) return this;
|
|
163
243
|
return this.mut(g).set("index", index);
|
|
@@ -204,9 +284,22 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
|
|
|
204
284
|
return node;
|
|
205
285
|
}
|
|
206
286
|
delSelf(g) {
|
|
287
|
+
if (this.aligned.in)
|
|
288
|
+
g.getNode(this.aligned.in).setAligned(g, "out", void 0);
|
|
289
|
+
if (this.aligned.out)
|
|
290
|
+
g.getNode(this.aligned.out).setAligned(g, "in", void 0);
|
|
207
291
|
this.getLayer(g).delNode(g, this.id);
|
|
208
|
-
for (const
|
|
209
|
-
|
|
292
|
+
for (const edge of this.rels(g, "edges", "both"))
|
|
293
|
+
edge.delSelf(g);
|
|
294
|
+
const remainingSegIds = [...this.segs.in, ...this.segs.out];
|
|
295
|
+
if (remainingSegIds.length > 0) {
|
|
296
|
+
for (const segId of remainingSegIds) {
|
|
297
|
+
if (g.segs?.has?.(segId)) {
|
|
298
|
+
g.getSeg(segId).delSelf(g);
|
|
299
|
+
} else {
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
210
303
|
g.nodes.delete(this.id);
|
|
211
304
|
g.dirtyNodes.delete(this.id);
|
|
212
305
|
g.delNodes.add(this.id);
|
|
@@ -299,30 +392,7 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
|
|
|
299
392
|
|
|
300
393
|
// src/graph/edge.ts
|
|
301
394
|
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");
|
|
395
|
+
var log3 = logger("edge");
|
|
326
396
|
var defEdgeData = {
|
|
327
397
|
id: "",
|
|
328
398
|
data: null,
|
|
@@ -330,7 +400,6 @@ var defEdgeData = {
|
|
|
330
400
|
source: { id: "" },
|
|
331
401
|
target: { id: "" },
|
|
332
402
|
type: void 0,
|
|
333
|
-
style: void 0,
|
|
334
403
|
mutable: false,
|
|
335
404
|
segIds: []
|
|
336
405
|
};
|
|
@@ -411,7 +480,7 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
|
|
|
411
480
|
source = edge.source.id;
|
|
412
481
|
if (edge.source?.port)
|
|
413
482
|
source = `${source}.${edge.source.port}`;
|
|
414
|
-
const marker = edge.source?.marker
|
|
483
|
+
const marker = edge.source?.marker;
|
|
415
484
|
if (marker && marker != "none") source += `[${marker}]`;
|
|
416
485
|
source += "-";
|
|
417
486
|
}
|
|
@@ -421,7 +490,7 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
|
|
|
421
490
|
if (edge.target.port)
|
|
422
491
|
target = `${target}.${edge.target.port}`;
|
|
423
492
|
target = "-" + target;
|
|
424
|
-
const marker = edge.target?.marker ??
|
|
493
|
+
const marker = edge.target?.marker ?? "arrow";
|
|
425
494
|
if (marker && marker != "none") target += `[${marker}]`;
|
|
426
495
|
}
|
|
427
496
|
const type = edge.type || "";
|
|
@@ -463,7 +532,6 @@ var defSegData = {
|
|
|
463
532
|
source: { id: "" },
|
|
464
533
|
target: { id: "" },
|
|
465
534
|
type: void 0,
|
|
466
|
-
style: void 0,
|
|
467
535
|
edgeIds: (0, import_immutable3.Set)(),
|
|
468
536
|
trackPos: void 0,
|
|
469
537
|
svg: void 0,
|
|
@@ -494,7 +562,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
|
|
|
494
562
|
sameEnd(other, side) {
|
|
495
563
|
const mine = this[side];
|
|
496
564
|
const yours = other[side];
|
|
497
|
-
return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
|
|
565
|
+
return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker && this.type === other.type;
|
|
498
566
|
}
|
|
499
567
|
setPos(g, source, target) {
|
|
500
568
|
return this.mut(g).merge({
|
|
@@ -566,7 +634,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
|
|
|
566
634
|
|
|
567
635
|
// src/graph/layer.ts
|
|
568
636
|
var import_immutable4 = require("immutable");
|
|
569
|
-
var
|
|
637
|
+
var log4 = logger("layer");
|
|
570
638
|
var defLayerData = {
|
|
571
639
|
id: "",
|
|
572
640
|
index: 0,
|
|
@@ -591,7 +659,7 @@ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
|
|
|
591
659
|
mutable: false
|
|
592
660
|
}).asImmutable();
|
|
593
661
|
}
|
|
594
|
-
get
|
|
662
|
+
get nodeCount() {
|
|
595
663
|
return this.nodeIds.size;
|
|
596
664
|
}
|
|
597
665
|
*nodes(g) {
|
|
@@ -646,9 +714,8 @@ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
|
|
|
646
714
|
reindex(g, nodeId) {
|
|
647
715
|
if (!this.isSorted) return void 0;
|
|
648
716
|
const sorted = this.sorted.filter((id) => id != nodeId);
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
g.getNode(sorted[i]).setIndex(g, i);
|
|
717
|
+
for (const [i, id] of this.sorted.entries())
|
|
718
|
+
g.getNode(id).setIndex(g, i);
|
|
652
719
|
return sorted;
|
|
653
720
|
}
|
|
654
721
|
delNode(g, nodeId) {
|
|
@@ -820,12 +887,12 @@ var Cycles = class _Cycles {
|
|
|
820
887
|
|
|
821
888
|
// src/graph/services/dummy.ts
|
|
822
889
|
var import_immutable5 = require("immutable");
|
|
823
|
-
var
|
|
890
|
+
var log5 = logger("dummy");
|
|
824
891
|
var Dummy = class _Dummy {
|
|
825
892
|
static updateDummies(g) {
|
|
826
893
|
for (const edgeId of g.dirtyEdges) {
|
|
827
894
|
const edge = g.getEdge(edgeId);
|
|
828
|
-
const { type
|
|
895
|
+
const { type } = edge;
|
|
829
896
|
const sourceLayer = edge.sourceNode(g).layerIndex(g);
|
|
830
897
|
const targetLayer = edge.targetNode(g).layerIndex(g);
|
|
831
898
|
let segIndex = 0;
|
|
@@ -849,7 +916,7 @@ var Dummy = class _Dummy {
|
|
|
849
916
|
});
|
|
850
917
|
target = { id: dummy.id };
|
|
851
918
|
}
|
|
852
|
-
seg = Seg.add(g, { source, target, type,
|
|
919
|
+
seg = Seg.add(g, { source, target, type, edgeIds: (0, import_immutable5.Set)([edgeId]) });
|
|
853
920
|
segs.splice(segIndex, 0, seg.id);
|
|
854
921
|
changed = true;
|
|
855
922
|
} 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 +955,8 @@ var Dummy = class _Dummy {
|
|
|
888
955
|
let layer = g.getLayer(layerId);
|
|
889
956
|
const groups = /* @__PURE__ */ new Map();
|
|
890
957
|
for (const nodeId of layer.nodeIds) {
|
|
891
|
-
if (!Node.isDummyId(nodeId)) continue;
|
|
892
958
|
const node = g.getNode(nodeId);
|
|
893
|
-
if (node.isMerged) continue;
|
|
959
|
+
if (!node.isDummy || node.isMerged) continue;
|
|
894
960
|
const edge = g.getEdge(node.edgeIds[0]);
|
|
895
961
|
const key = Edge.key(edge, "k:", side);
|
|
896
962
|
if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
|
|
@@ -898,7 +964,7 @@ var Dummy = class _Dummy {
|
|
|
898
964
|
}
|
|
899
965
|
for (const [key, group] of groups) {
|
|
900
966
|
if (group.size == 1) continue;
|
|
901
|
-
const edgeIds = [...group].map((node) => node.
|
|
967
|
+
const edgeIds = [...group].map((node) => node.edgeIds[0]);
|
|
902
968
|
const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
|
|
903
969
|
let seg;
|
|
904
970
|
for (const old of group) {
|
|
@@ -908,8 +974,9 @@ var Dummy = class _Dummy {
|
|
|
908
974
|
const example = g.getSeg(segId);
|
|
909
975
|
seg = Seg.add(g, {
|
|
910
976
|
...example,
|
|
911
|
-
edgeIds: (0, import_immutable5.Set)(
|
|
912
|
-
[
|
|
977
|
+
edgeIds: (0, import_immutable5.Set)(edgeIds),
|
|
978
|
+
[side]: { ...example[side] },
|
|
979
|
+
[altSide]: { ...example[altSide], id: dummy.id, port: void 0 }
|
|
913
980
|
});
|
|
914
981
|
}
|
|
915
982
|
edge = edge.replaceSegId(g, segId, seg.id);
|
|
@@ -921,12 +988,15 @@ var Dummy = class _Dummy {
|
|
|
921
988
|
const example = g.getSeg(segId);
|
|
922
989
|
const seg2 = Seg.add(g, {
|
|
923
990
|
...example,
|
|
924
|
-
edgeIds: (0, import_immutable5.Set)([old.
|
|
925
|
-
[
|
|
991
|
+
edgeIds: (0, import_immutable5.Set)([old.edgeIds[0]]),
|
|
992
|
+
[altSide]: { ...example[altSide] },
|
|
993
|
+
[side]: { ...example[side], id: dummy.id, port: void 0 }
|
|
926
994
|
});
|
|
927
995
|
edge = edge.replaceSegId(g, segId, seg2.id);
|
|
928
996
|
}
|
|
929
997
|
}
|
|
998
|
+
for (const old of group)
|
|
999
|
+
old.delSelf(g);
|
|
930
1000
|
}
|
|
931
1001
|
}
|
|
932
1002
|
}
|
|
@@ -934,7 +1004,7 @@ var Dummy = class _Dummy {
|
|
|
934
1004
|
|
|
935
1005
|
// src/graph/services/layers.ts
|
|
936
1006
|
var import_immutable6 = require("immutable");
|
|
937
|
-
var
|
|
1007
|
+
var log6 = logger("layers");
|
|
938
1008
|
var Layers = class {
|
|
939
1009
|
static updateLayers(g) {
|
|
940
1010
|
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 +1067,7 @@ var Layers = class {
|
|
|
997
1067
|
|
|
998
1068
|
// src/graph/services/layout.ts
|
|
999
1069
|
var import_immutable7 = require("immutable");
|
|
1000
|
-
var
|
|
1070
|
+
var log7 = logger("layout");
|
|
1001
1071
|
var Layout = class _Layout {
|
|
1002
1072
|
static parentIndex(g, node) {
|
|
1003
1073
|
const parents = (0, import_immutable7.Seq)([...node.adjs(g, "segs", "in")]);
|
|
@@ -1014,8 +1084,8 @@ var Layout = class _Layout {
|
|
|
1014
1084
|
if (a.isDummy && !b.isDummy) return -1;
|
|
1015
1085
|
if (!a.isDummy && b.isDummy) return 1;
|
|
1016
1086
|
if (!a.isDummy) return a.id.localeCompare(b.id);
|
|
1017
|
-
const minA =
|
|
1018
|
-
const minB =
|
|
1087
|
+
const minA = (0, import_immutable7.Seq)(a.edgeIds).min();
|
|
1088
|
+
const minB = (0, import_immutable7.Seq)(b.edgeIds).min();
|
|
1019
1089
|
return minA.localeCompare(minB);
|
|
1020
1090
|
}
|
|
1021
1091
|
static positionNodes(g) {
|
|
@@ -1041,7 +1111,12 @@ var Layout = class _Layout {
|
|
|
1041
1111
|
let node = g.getNode(sorted[i]);
|
|
1042
1112
|
node = node.setIndex(g, i).setLayerPos(g, lpos);
|
|
1043
1113
|
const size = node.dims?.[g.w] ?? 0;
|
|
1044
|
-
|
|
1114
|
+
let margin = node.margin(g);
|
|
1115
|
+
if (i + 1 < sorted.length) {
|
|
1116
|
+
const next = g.getNode(sorted[i + 1]);
|
|
1117
|
+
margin = node.marginWith(g, next);
|
|
1118
|
+
}
|
|
1119
|
+
lpos += size + margin;
|
|
1045
1120
|
}
|
|
1046
1121
|
}
|
|
1047
1122
|
}
|
|
@@ -1073,7 +1148,7 @@ var Layout = class _Layout {
|
|
|
1073
1148
|
let iterations = 0;
|
|
1074
1149
|
while (true) {
|
|
1075
1150
|
if (++iterations > 10) {
|
|
1076
|
-
|
|
1151
|
+
log7.error(`alignNodes: infinite loop detected in layer ${layerId}`);
|
|
1077
1152
|
break;
|
|
1078
1153
|
}
|
|
1079
1154
|
let changed = false;
|
|
@@ -1133,7 +1208,7 @@ var Layout = class _Layout {
|
|
|
1133
1208
|
static anchorPos(g, seg, side) {
|
|
1134
1209
|
const nodeId = seg[side].id;
|
|
1135
1210
|
const node = g.getNode(nodeId);
|
|
1136
|
-
let p = {
|
|
1211
|
+
let p = { [g.x]: node.lpos, [g.y]: node.pos?.[g.y] ?? 0 };
|
|
1137
1212
|
let w = node.dims?.[g.w] ?? 0;
|
|
1138
1213
|
let h = node.dims?.[g.h] ?? 0;
|
|
1139
1214
|
if (node.isDummy)
|
|
@@ -1179,22 +1254,18 @@ var Layout = class _Layout {
|
|
|
1179
1254
|
}
|
|
1180
1255
|
static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
|
|
1181
1256
|
const node = g.getNode(nodeId);
|
|
1182
|
-
log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
|
|
1183
1257
|
if (!conservative)
|
|
1184
1258
|
_Layout.markAligned(g, nodeId, alignId, dir, lpos);
|
|
1185
|
-
const
|
|
1186
|
-
const nodeWidth = node.dims?.[g.w] ?? 0;
|
|
1187
|
-
const aMin = lpos - space, aMax = lpos + nodeWidth + space;
|
|
1259
|
+
const nodeRight = lpos + node.width(g);
|
|
1188
1260
|
repeat:
|
|
1189
1261
|
for (const otherId of node.getLayer(g).nodeIds) {
|
|
1190
1262
|
if (otherId == nodeId) continue;
|
|
1191
1263
|
const other = g.getNode(otherId);
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
if (aMin < bMax && bMin < aMax) {
|
|
1264
|
+
const margin = node.marginWith(g, other);
|
|
1265
|
+
const gap = lpos < other.lpos ? other.lpos - nodeRight : lpos - other.right(g);
|
|
1266
|
+
if (gap < margin) {
|
|
1196
1267
|
if (conservative) return false;
|
|
1197
|
-
const safePos = reverseMove ?
|
|
1268
|
+
const safePos = reverseMove ? lpos - other.width(g) - margin : nodeRight + margin;
|
|
1198
1269
|
_Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
|
|
1199
1270
|
continue repeat;
|
|
1200
1271
|
}
|
|
@@ -1243,11 +1314,12 @@ var Layout = class _Layout {
|
|
|
1243
1314
|
for (const layerId of g.layerList) {
|
|
1244
1315
|
const layer = g.getLayer(layerId);
|
|
1245
1316
|
if (layer.sorted.length < 2) continue;
|
|
1246
|
-
for (const nodeId of layer.sorted) {
|
|
1317
|
+
for (const [i, nodeId] of layer.sorted.entries()) {
|
|
1247
1318
|
const node = g.getNode(nodeId);
|
|
1248
1319
|
if (node.index == 0) continue;
|
|
1249
1320
|
let minGap = Infinity;
|
|
1250
1321
|
const stack = [];
|
|
1322
|
+
let maxMargin = 0;
|
|
1251
1323
|
for (const right of _Layout.aligned(g, nodeId, "both")) {
|
|
1252
1324
|
stack.push(right);
|
|
1253
1325
|
const leftId = _Layout.leftOf(g, right);
|
|
@@ -1256,8 +1328,10 @@ var Layout = class _Layout {
|
|
|
1256
1328
|
const leftWidth = left.dims?.[g.w] ?? 0;
|
|
1257
1329
|
const gap = right.lpos - left.lpos - leftWidth;
|
|
1258
1330
|
if (gap < minGap) minGap = gap;
|
|
1331
|
+
const margin = right.marginWith(g, left);
|
|
1332
|
+
if (margin > maxMargin) maxMargin = margin;
|
|
1259
1333
|
}
|
|
1260
|
-
const delta = minGap -
|
|
1334
|
+
const delta = minGap - maxMargin;
|
|
1261
1335
|
if (delta <= 0) continue;
|
|
1262
1336
|
anyChanged = true;
|
|
1263
1337
|
for (const right of stack)
|
|
@@ -1305,9 +1379,8 @@ var Layout = class _Layout {
|
|
|
1305
1379
|
};
|
|
1306
1380
|
|
|
1307
1381
|
// src/canvas/marker.tsx
|
|
1308
|
-
var import_marker = __toESM(require("./marker.css?raw"), 1);
|
|
1309
1382
|
var import_jsx_runtime = require("jsx-dom/jsx-runtime");
|
|
1310
|
-
function arrow(size,
|
|
1383
|
+
function arrow(size, reverse = false) {
|
|
1311
1384
|
const h = size / 1.5;
|
|
1312
1385
|
const w = size;
|
|
1313
1386
|
const ry = h / 2;
|
|
@@ -1316,7 +1389,7 @@ function arrow(size, classPrefix, reverse = false) {
|
|
|
1316
1389
|
"marker",
|
|
1317
1390
|
{
|
|
1318
1391
|
id: `g3p-marker-arrow${suffix}`,
|
|
1319
|
-
className:
|
|
1392
|
+
className: "g3p-marker g3p-marker-arrow",
|
|
1320
1393
|
markerWidth: size,
|
|
1321
1394
|
markerHeight: size,
|
|
1322
1395
|
refX: "2",
|
|
@@ -1327,7 +1400,7 @@ function arrow(size, classPrefix, reverse = false) {
|
|
|
1327
1400
|
}
|
|
1328
1401
|
);
|
|
1329
1402
|
}
|
|
1330
|
-
function circle(size,
|
|
1403
|
+
function circle(size, reverse = false) {
|
|
1331
1404
|
const r = size / 3;
|
|
1332
1405
|
const cy = size / 2;
|
|
1333
1406
|
const suffix = reverse ? "-reverse" : "";
|
|
@@ -1335,7 +1408,7 @@ function circle(size, classPrefix, reverse = false) {
|
|
|
1335
1408
|
"marker",
|
|
1336
1409
|
{
|
|
1337
1410
|
id: `g3p-marker-circle${suffix}`,
|
|
1338
|
-
className:
|
|
1411
|
+
className: "g3p-marker g3p-marker-circle",
|
|
1339
1412
|
markerWidth: size,
|
|
1340
1413
|
markerHeight: size,
|
|
1341
1414
|
refX: "2",
|
|
@@ -1346,7 +1419,7 @@ function circle(size, classPrefix, reverse = false) {
|
|
|
1346
1419
|
}
|
|
1347
1420
|
);
|
|
1348
1421
|
}
|
|
1349
|
-
function diamond(size,
|
|
1422
|
+
function diamond(size, reverse = false) {
|
|
1350
1423
|
const w = size * 0.7;
|
|
1351
1424
|
const h = size / 2;
|
|
1352
1425
|
const cy = size / 2;
|
|
@@ -1355,7 +1428,7 @@ function diamond(size, classPrefix, reverse = false) {
|
|
|
1355
1428
|
"marker",
|
|
1356
1429
|
{
|
|
1357
1430
|
id: `g3p-marker-diamond${suffix}`,
|
|
1358
|
-
className:
|
|
1431
|
+
className: "g3p-marker g3p-marker-diamond",
|
|
1359
1432
|
markerWidth: size,
|
|
1360
1433
|
markerHeight: size,
|
|
1361
1434
|
refX: "2",
|
|
@@ -1366,7 +1439,7 @@ function diamond(size, classPrefix, reverse = false) {
|
|
|
1366
1439
|
}
|
|
1367
1440
|
);
|
|
1368
1441
|
}
|
|
1369
|
-
function bar(size,
|
|
1442
|
+
function bar(size, reverse = false) {
|
|
1370
1443
|
const h = size * 0.6;
|
|
1371
1444
|
const cy = size / 2;
|
|
1372
1445
|
const suffix = reverse ? "-reverse" : "";
|
|
@@ -1374,7 +1447,7 @@ function bar(size, classPrefix, reverse = false) {
|
|
|
1374
1447
|
"marker",
|
|
1375
1448
|
{
|
|
1376
1449
|
id: `g3p-marker-bar${suffix}`,
|
|
1377
|
-
className:
|
|
1450
|
+
className: "g3p-marker g3p-marker-bar",
|
|
1378
1451
|
markerWidth: size,
|
|
1379
1452
|
markerHeight: size,
|
|
1380
1453
|
refX: "2",
|
|
@@ -1385,7 +1458,7 @@ function bar(size, classPrefix, reverse = false) {
|
|
|
1385
1458
|
}
|
|
1386
1459
|
);
|
|
1387
1460
|
}
|
|
1388
|
-
function none(size,
|
|
1461
|
+
function none(size, reverse = false) {
|
|
1389
1462
|
return void 0;
|
|
1390
1463
|
}
|
|
1391
1464
|
function normalize(data) {
|
|
@@ -1404,7 +1477,7 @@ var markerDefs = {
|
|
|
1404
1477
|
};
|
|
1405
1478
|
|
|
1406
1479
|
// src/graph/services/lines.ts
|
|
1407
|
-
var
|
|
1480
|
+
var log8 = logger("lines");
|
|
1408
1481
|
var Lines = class _Lines {
|
|
1409
1482
|
static layoutSeg(g, seg) {
|
|
1410
1483
|
const sourcePos = Layout.anchorPos(g, seg, "source");
|
|
@@ -1450,7 +1523,11 @@ var Lines = class _Lines {
|
|
|
1450
1523
|
validTrack = track;
|
|
1451
1524
|
break;
|
|
1452
1525
|
}
|
|
1453
|
-
|
|
1526
|
+
const minA = Math.min(seg.p1, seg.p2);
|
|
1527
|
+
const maxA = Math.max(seg.p1, seg.p2);
|
|
1528
|
+
const minB = Math.min(other.p1, other.p2);
|
|
1529
|
+
const maxB = Math.max(other.p1, other.p2);
|
|
1530
|
+
if (minA < maxB && minB < maxA) {
|
|
1454
1531
|
overlap = true;
|
|
1455
1532
|
break;
|
|
1456
1533
|
}
|
|
@@ -1465,6 +1542,21 @@ var Lines = class _Lines {
|
|
|
1465
1542
|
else
|
|
1466
1543
|
trackSet.push([seg]);
|
|
1467
1544
|
}
|
|
1545
|
+
const midpoint = (s) => (s.p1 + s.p2) / 2;
|
|
1546
|
+
const sortTracks = (tracks2, goingRight) => {
|
|
1547
|
+
tracks2.sort((a, b) => {
|
|
1548
|
+
const midA = Math.max(...a.map(midpoint));
|
|
1549
|
+
const midB = Math.max(...b.map(midpoint));
|
|
1550
|
+
return goingRight ? midB - midA : midA - midB;
|
|
1551
|
+
});
|
|
1552
|
+
};
|
|
1553
|
+
sortTracks(rightTracks, true);
|
|
1554
|
+
sortTracks(leftTracks, false);
|
|
1555
|
+
allTracks.sort((a, b) => {
|
|
1556
|
+
const avgA = a.reduce((sum, s) => sum + midpoint(s), 0) / a.length;
|
|
1557
|
+
const avgB = b.reduce((sum, s) => sum + midpoint(s), 0) / b.length;
|
|
1558
|
+
return avgB - avgA;
|
|
1559
|
+
});
|
|
1468
1560
|
const tracks = [];
|
|
1469
1561
|
const all = leftTracks.concat(rightTracks).concat(allTracks);
|
|
1470
1562
|
for (const track of all)
|
|
@@ -1635,6 +1727,7 @@ var Lines = class _Lines {
|
|
|
1635
1727
|
};
|
|
1636
1728
|
|
|
1637
1729
|
// src/graph/graph.ts
|
|
1730
|
+
var log9 = logger("graph");
|
|
1638
1731
|
var emptyChanges = {
|
|
1639
1732
|
addedNodes: [],
|
|
1640
1733
|
removedNodes: [],
|
|
@@ -1909,29 +2002,8 @@ var screenPos = (x, y) => ({ x, y });
|
|
|
1909
2002
|
var canvasPos = (x, y) => ({ x, y });
|
|
1910
2003
|
var graphPos = (x, y) => ({ x, y });
|
|
1911
2004
|
|
|
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
2005
|
// src/canvas/node.tsx
|
|
1933
2006
|
var import_jsx_runtime2 = require("jsx-dom/jsx-runtime");
|
|
1934
|
-
var log8 = logger("canvas");
|
|
1935
2007
|
var Node2 = class {
|
|
1936
2008
|
selected;
|
|
1937
2009
|
hovered;
|
|
@@ -1939,7 +2011,6 @@ var Node2 = class {
|
|
|
1939
2011
|
content;
|
|
1940
2012
|
canvas;
|
|
1941
2013
|
data;
|
|
1942
|
-
classPrefix;
|
|
1943
2014
|
isDummy;
|
|
1944
2015
|
pos;
|
|
1945
2016
|
constructor(canvas, data, isDummy = false) {
|
|
@@ -1947,20 +2018,18 @@ var Node2 = class {
|
|
|
1947
2018
|
this.data = data;
|
|
1948
2019
|
this.selected = false;
|
|
1949
2020
|
this.hovered = false;
|
|
1950
|
-
this.classPrefix = canvas.classPrefix;
|
|
1951
2021
|
this.isDummy = isDummy;
|
|
1952
2022
|
if (this.isDummy) {
|
|
1953
2023
|
const size = canvas.dummyNodeSize;
|
|
1954
2024
|
} else {
|
|
1955
2025
|
const render = data.render ?? canvas.renderNode;
|
|
1956
|
-
this.content = this.renderContent(render(data.data));
|
|
2026
|
+
this.content = this.renderContent(render(data.data, data));
|
|
1957
2027
|
}
|
|
1958
2028
|
}
|
|
1959
2029
|
remove() {
|
|
1960
2030
|
this.container.remove();
|
|
1961
2031
|
}
|
|
1962
2032
|
append() {
|
|
1963
|
-
console.log("append", this);
|
|
1964
2033
|
this.canvas.group.appendChild(this.container);
|
|
1965
2034
|
}
|
|
1966
2035
|
needsContentSize() {
|
|
@@ -1969,19 +2038,6 @@ var Node2 = class {
|
|
|
1969
2038
|
needsContainerSize() {
|
|
1970
2039
|
return !this.isDummy;
|
|
1971
2040
|
}
|
|
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
2041
|
setPos(pos) {
|
|
1986
2042
|
this.pos = pos;
|
|
1987
2043
|
const { x, y } = pos;
|
|
@@ -1998,7 +2054,6 @@ var Node2 = class {
|
|
|
1998
2054
|
return el;
|
|
1999
2055
|
}
|
|
2000
2056
|
renderContainer() {
|
|
2001
|
-
const c = styler("node", import_node3.default, this.classPrefix);
|
|
2002
2057
|
const hasPorts = this.hasPorts();
|
|
2003
2058
|
const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
|
|
2004
2059
|
const nodeType = this.data?.type;
|
|
@@ -2006,14 +2061,8 @@ var Node2 = class {
|
|
|
2006
2061
|
this.container = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2007
2062
|
"g",
|
|
2008
2063
|
{
|
|
2009
|
-
className:
|
|
2010
|
-
|
|
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" },
|
|
2064
|
+
className: `g3p-node-container ${this.isDummy ? "g3p-node-dummy" : ""} ${typeClass}`.trim(),
|
|
2065
|
+
"data-node-id": this.data?.id,
|
|
2017
2066
|
children: inner
|
|
2018
2067
|
}
|
|
2019
2068
|
);
|
|
@@ -2023,7 +2072,6 @@ var Node2 = class {
|
|
|
2023
2072
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("foreignObject", { width: w, height: h, children: this.content });
|
|
2024
2073
|
}
|
|
2025
2074
|
renderDummy() {
|
|
2026
|
-
const c = styler("node", import_node3.default, this.classPrefix);
|
|
2027
2075
|
let w = this.canvas.dummyNodeSize;
|
|
2028
2076
|
let h = this.canvas.dummyNodeSize;
|
|
2029
2077
|
w /= 2;
|
|
@@ -2036,7 +2084,7 @@ var Node2 = class {
|
|
|
2036
2084
|
cy: h,
|
|
2037
2085
|
rx: w,
|
|
2038
2086
|
ry: h,
|
|
2039
|
-
className:
|
|
2087
|
+
className: "g3p-node-background"
|
|
2040
2088
|
}
|
|
2041
2089
|
),
|
|
2042
2090
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -2047,7 +2095,7 @@ var Node2 = class {
|
|
|
2047
2095
|
rx: w,
|
|
2048
2096
|
ry: h,
|
|
2049
2097
|
fill: "none",
|
|
2050
|
-
className:
|
|
2098
|
+
className: "g3p-node-border"
|
|
2051
2099
|
}
|
|
2052
2100
|
)
|
|
2053
2101
|
] });
|
|
@@ -2060,7 +2108,7 @@ var Node2 = class {
|
|
|
2060
2108
|
const ports = data.ports?.[dir];
|
|
2061
2109
|
if (!ports) continue;
|
|
2062
2110
|
for (const port of ports) {
|
|
2063
|
-
const el = this.content.querySelector(
|
|
2111
|
+
const el = this.content.querySelector(`.g3p-node-port[data-node-id="${data.id}"][data-port-id="${port.id}"]`);
|
|
2064
2112
|
if (!el) continue;
|
|
2065
2113
|
const portRect = el.getBoundingClientRect();
|
|
2066
2114
|
if (isVertical) {
|
|
@@ -2098,23 +2146,22 @@ var Node2 = class {
|
|
|
2098
2146
|
renderPortRow(dir, inout) {
|
|
2099
2147
|
const ports = this.data?.ports?.[dir];
|
|
2100
2148
|
if (!ports?.length) return null;
|
|
2101
|
-
const c = styler("node", import_node3.default, this.classPrefix);
|
|
2102
2149
|
const pos = this.getPortPosition(dir);
|
|
2103
2150
|
const isVertical = this.isVerticalOrientation();
|
|
2104
2151
|
const layoutClass = isVertical ? "row" : "col";
|
|
2105
2152
|
const rotateLabels = false;
|
|
2106
2153
|
const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
|
|
2107
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className:
|
|
2154
|
+
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
2155
|
"div",
|
|
2109
2156
|
{
|
|
2110
|
-
|
|
2111
|
-
|
|
2157
|
+
className: `g3p-node-port g3p-node-port-${inout}-${pos} ${rotateClass}`,
|
|
2158
|
+
"data-node-id": this.data.id,
|
|
2159
|
+
"data-port-id": port.id,
|
|
2112
2160
|
children: port.label ?? port.id
|
|
2113
2161
|
}
|
|
2114
2162
|
)) });
|
|
2115
2163
|
}
|
|
2116
2164
|
renderInsidePorts(el) {
|
|
2117
|
-
const c = styler("node", import_node3.default, this.classPrefix);
|
|
2118
2165
|
const isVertical = this.isVerticalOrientation();
|
|
2119
2166
|
const isReversed = this.isReversedOrientation();
|
|
2120
2167
|
let inPorts = this.renderPortRow("in", "in");
|
|
@@ -2122,14 +2169,13 @@ var Node2 = class {
|
|
|
2122
2169
|
if (!inPorts && !outPorts) return el;
|
|
2123
2170
|
if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
|
|
2124
2171
|
const wrapperClass = isVertical ? "v" : "h";
|
|
2125
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className:
|
|
2172
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
|
|
2126
2173
|
inPorts,
|
|
2127
2174
|
el,
|
|
2128
2175
|
outPorts
|
|
2129
2176
|
] });
|
|
2130
2177
|
}
|
|
2131
2178
|
renderOutsidePorts(el) {
|
|
2132
|
-
const c = styler("node", import_node3.default, this.classPrefix);
|
|
2133
2179
|
const isVertical = this.isVerticalOrientation();
|
|
2134
2180
|
const isReversed = this.isReversedOrientation();
|
|
2135
2181
|
let inPorts = this.renderPortRow("in", "out");
|
|
@@ -2137,71 +2183,59 @@ var Node2 = class {
|
|
|
2137
2183
|
if (!inPorts && !outPorts) return el;
|
|
2138
2184
|
if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
|
|
2139
2185
|
const wrapperClass = isVertical ? "v" : "h";
|
|
2140
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className:
|
|
2186
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
|
|
2141
2187
|
inPorts,
|
|
2142
2188
|
el,
|
|
2143
2189
|
outPorts
|
|
2144
2190
|
] });
|
|
2145
2191
|
}
|
|
2146
2192
|
renderBorder(el) {
|
|
2147
|
-
|
|
2148
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: c("border"), children: el });
|
|
2193
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "g3p-node-border", children: el });
|
|
2149
2194
|
}
|
|
2150
2195
|
};
|
|
2151
2196
|
|
|
2152
2197
|
// src/canvas/seg.tsx
|
|
2153
|
-
var import_seg3 = __toESM(require("./seg.css?raw"), 1);
|
|
2154
2198
|
var import_jsx_runtime3 = require("jsx-dom/jsx-runtime");
|
|
2155
|
-
var log9 = logger("canvas");
|
|
2156
2199
|
var Seg2 = class {
|
|
2157
2200
|
id;
|
|
2158
2201
|
selected;
|
|
2159
2202
|
hovered;
|
|
2160
2203
|
canvas;
|
|
2161
|
-
classPrefix;
|
|
2162
2204
|
type;
|
|
2163
2205
|
svg;
|
|
2164
2206
|
el;
|
|
2165
2207
|
source;
|
|
2166
2208
|
target;
|
|
2209
|
+
edgeIds;
|
|
2167
2210
|
constructor(canvas, data, g) {
|
|
2168
2211
|
this.id = data.id;
|
|
2169
2212
|
this.canvas = canvas;
|
|
2170
2213
|
this.selected = false;
|
|
2171
2214
|
this.hovered = false;
|
|
2172
2215
|
this.svg = data.svg;
|
|
2173
|
-
this.classPrefix = canvas.classPrefix;
|
|
2174
2216
|
this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
|
|
2175
2217
|
this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
|
|
2176
2218
|
this.type = data.type;
|
|
2219
|
+
this.edgeIds = data.edgeIds.toArray();
|
|
2177
2220
|
this.el = this.render();
|
|
2178
2221
|
}
|
|
2179
|
-
handleClick(e) {
|
|
2180
|
-
e.stopPropagation();
|
|
2181
|
-
}
|
|
2182
|
-
handleMouseEnter(e) {
|
|
2183
|
-
}
|
|
2184
|
-
handleMouseLeave(e) {
|
|
2185
|
-
}
|
|
2186
|
-
handleContextMenu(e) {
|
|
2187
|
-
}
|
|
2188
2222
|
append() {
|
|
2189
2223
|
this.canvas.group.appendChild(this.el);
|
|
2190
2224
|
}
|
|
2191
2225
|
remove() {
|
|
2192
2226
|
this.el.remove();
|
|
2193
2227
|
}
|
|
2194
|
-
update(data) {
|
|
2228
|
+
update(data, g) {
|
|
2195
2229
|
this.svg = data.svg;
|
|
2196
2230
|
this.type = data.type;
|
|
2197
|
-
this.source = data.source;
|
|
2198
|
-
this.target = data.target;
|
|
2231
|
+
this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
|
|
2232
|
+
this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
|
|
2233
|
+
this.edgeIds = data.edgeIds.toArray();
|
|
2199
2234
|
this.remove();
|
|
2200
2235
|
this.el = this.render();
|
|
2201
2236
|
this.append();
|
|
2202
2237
|
}
|
|
2203
2238
|
render() {
|
|
2204
|
-
const c = styler("seg", import_seg3.default, this.classPrefix);
|
|
2205
2239
|
let { source, target } = normalize(this);
|
|
2206
2240
|
if (this.source.isDummy) source = void 0;
|
|
2207
2241
|
if (this.target.isDummy) target = void 0;
|
|
@@ -2211,18 +2245,15 @@ var Seg2 = class {
|
|
|
2211
2245
|
{
|
|
2212
2246
|
ref: (el) => this.el = el,
|
|
2213
2247
|
id: `g3p-seg-${this.id}`,
|
|
2214
|
-
className:
|
|
2215
|
-
|
|
2216
|
-
onMouseEnter: this.handleMouseEnter.bind(this),
|
|
2217
|
-
onMouseLeave: this.handleMouseLeave.bind(this),
|
|
2218
|
-
onContextMenu: this.handleContextMenu.bind(this),
|
|
2248
|
+
className: `g3p-seg-container ${typeClass}`.trim(),
|
|
2249
|
+
"data-edge-id": this.id,
|
|
2219
2250
|
children: [
|
|
2220
2251
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2221
2252
|
"path",
|
|
2222
2253
|
{
|
|
2223
2254
|
d: this.svg,
|
|
2224
2255
|
fill: "none",
|
|
2225
|
-
className:
|
|
2256
|
+
className: "g3p-seg-line",
|
|
2226
2257
|
markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
|
|
2227
2258
|
markerEnd: target ? `url(#g3p-marker-${target})` : void 0
|
|
2228
2259
|
}
|
|
@@ -2233,7 +2264,7 @@ var Seg2 = class {
|
|
|
2233
2264
|
d: this.svg,
|
|
2234
2265
|
stroke: "transparent",
|
|
2235
2266
|
fill: "none",
|
|
2236
|
-
className:
|
|
2267
|
+
className: "g3p-seg-hitbox",
|
|
2237
2268
|
style: { cursor: "pointer" }
|
|
2238
2269
|
}
|
|
2239
2270
|
)
|
|
@@ -2243,46 +2274,561 @@ var Seg2 = class {
|
|
|
2243
2274
|
}
|
|
2244
2275
|
};
|
|
2245
2276
|
|
|
2246
|
-
// src/canvas/
|
|
2247
|
-
var
|
|
2248
|
-
|
|
2277
|
+
// src/canvas/editMode.ts
|
|
2278
|
+
var EditMode = class {
|
|
2279
|
+
_state = { type: "idle" };
|
|
2280
|
+
_editable = false;
|
|
2281
|
+
get state() {
|
|
2282
|
+
return this._state;
|
|
2283
|
+
}
|
|
2284
|
+
get editable() {
|
|
2285
|
+
return this._editable;
|
|
2286
|
+
}
|
|
2287
|
+
set editable(value) {
|
|
2288
|
+
this._editable = value;
|
|
2289
|
+
if (!value) {
|
|
2290
|
+
this.reset();
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
get isIdle() {
|
|
2294
|
+
return this._state.type === "idle";
|
|
2295
|
+
}
|
|
2296
|
+
get isPanning() {
|
|
2297
|
+
return this._state.type === "panning";
|
|
2298
|
+
}
|
|
2299
|
+
get isCreatingEdge() {
|
|
2300
|
+
return this._state.type === "new-edge";
|
|
2301
|
+
}
|
|
2302
|
+
/** Start panning the canvas */
|
|
2303
|
+
startPan(startCanvas, startTransform) {
|
|
2304
|
+
this._state = { type: "panning", startCanvas, startTransform };
|
|
2305
|
+
}
|
|
2306
|
+
/** Start creating a new edge from a node or port */
|
|
2307
|
+
startNewEdge(id, startGraph, port) {
|
|
2308
|
+
if (!this._editable) return;
|
|
2309
|
+
this._state = {
|
|
2310
|
+
type: "new-edge",
|
|
2311
|
+
source: { id, port },
|
|
2312
|
+
startGraph,
|
|
2313
|
+
currentGraph: startGraph,
|
|
2314
|
+
target: null
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
/** Update the current position during new-edge mode */
|
|
2318
|
+
updateNewEdgePosition(currentGraph) {
|
|
2319
|
+
if (this._state.type === "new-edge") {
|
|
2320
|
+
this._state = { ...this._state, currentGraph };
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
/** Update the hover target during new-edge mode */
|
|
2324
|
+
setHoverTarget(target) {
|
|
2325
|
+
if (this._state.type === "new-edge") {
|
|
2326
|
+
this._state = { ...this._state, target };
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
/** Get the new-edge state if active */
|
|
2330
|
+
getNewEdgeState() {
|
|
2331
|
+
if (this._state.type === "new-edge") {
|
|
2332
|
+
return this._state;
|
|
2333
|
+
}
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
/** Reset to idle state */
|
|
2337
|
+
reset() {
|
|
2338
|
+
this._state = { type: "idle" };
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2342
|
+
// src/canvas/newEdge.tsx
|
|
2249
2343
|
var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2344
|
+
function renderNewEdge({ start, end }) {
|
|
2345
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: "g3p-new-edge-container", children: [
|
|
2346
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2347
|
+
"circle",
|
|
2348
|
+
{
|
|
2349
|
+
cx: start.x,
|
|
2350
|
+
cy: start.y,
|
|
2351
|
+
r: 4,
|
|
2352
|
+
className: "g3p-new-edge-origin"
|
|
2353
|
+
}
|
|
2354
|
+
),
|
|
2355
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2356
|
+
"line",
|
|
2357
|
+
{
|
|
2358
|
+
x1: start.x,
|
|
2359
|
+
y1: start.y,
|
|
2360
|
+
x2: end.x,
|
|
2361
|
+
y2: end.y,
|
|
2362
|
+
className: "g3p-new-edge-line"
|
|
2363
|
+
}
|
|
2364
|
+
),
|
|
2365
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2366
|
+
"circle",
|
|
2367
|
+
{
|
|
2368
|
+
cx: end.x,
|
|
2369
|
+
cy: end.y,
|
|
2370
|
+
r: 3,
|
|
2371
|
+
className: "g3p-new-edge-end"
|
|
2372
|
+
}
|
|
2373
|
+
)
|
|
2374
|
+
] });
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// src/canvas/modal.tsx
|
|
2378
|
+
var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
|
|
2379
|
+
var Modal = class {
|
|
2380
|
+
container;
|
|
2381
|
+
overlay;
|
|
2382
|
+
dialog;
|
|
2383
|
+
onClose;
|
|
2384
|
+
mouseDownOnOverlay = false;
|
|
2385
|
+
constructor(options) {
|
|
2386
|
+
this.onClose = options.onClose;
|
|
2387
|
+
this.overlay = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2388
|
+
"div",
|
|
2389
|
+
{
|
|
2390
|
+
className: "g3p-modal-overlay",
|
|
2391
|
+
onMouseDown: (e) => {
|
|
2392
|
+
this.mouseDownOnOverlay = e.target === this.overlay;
|
|
2393
|
+
},
|
|
2394
|
+
onMouseUp: (e) => {
|
|
2395
|
+
if (this.mouseDownOnOverlay && e.target === this.overlay) this.close();
|
|
2396
|
+
this.mouseDownOnOverlay = false;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
);
|
|
2400
|
+
this.dialog = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-dialog", children: [
|
|
2401
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-header", children: [
|
|
2402
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "g3p-modal-title", children: options.title }),
|
|
2403
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2404
|
+
"button",
|
|
2405
|
+
{
|
|
2406
|
+
className: "g3p-modal-close",
|
|
2407
|
+
onClick: () => this.close(),
|
|
2408
|
+
children: "\xD7"
|
|
2409
|
+
}
|
|
2410
|
+
)
|
|
2411
|
+
] }),
|
|
2412
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-body" }),
|
|
2413
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-footer" })
|
|
2414
|
+
] });
|
|
2415
|
+
if (options.position) {
|
|
2416
|
+
this.dialog.style.position = "absolute";
|
|
2417
|
+
this.dialog.style.left = `${options.position.x}px`;
|
|
2418
|
+
this.dialog.style.top = `${options.position.y}px`;
|
|
2419
|
+
this.dialog.style.transform = "translate(-50%, -50%)";
|
|
2420
|
+
}
|
|
2421
|
+
this.overlay.appendChild(this.dialog);
|
|
2422
|
+
this.container = this.overlay;
|
|
2423
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
2424
|
+
document.addEventListener("keydown", this.handleKeyDown);
|
|
2425
|
+
}
|
|
2426
|
+
handleKeyDown(e) {
|
|
2427
|
+
if (e.key === "Escape") {
|
|
2428
|
+
this.close();
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
get body() {
|
|
2432
|
+
return this.dialog.querySelector(".g3p-modal-body");
|
|
2433
|
+
}
|
|
2434
|
+
get footer() {
|
|
2435
|
+
return this.dialog.querySelector(".g3p-modal-footer");
|
|
2436
|
+
}
|
|
2437
|
+
show(parent) {
|
|
2438
|
+
parent.appendChild(this.container);
|
|
2439
|
+
const firstInput = this.dialog.querySelector("input, select, button");
|
|
2440
|
+
if (firstInput) firstInput.focus();
|
|
2441
|
+
}
|
|
2442
|
+
close() {
|
|
2443
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2444
|
+
this.container.remove();
|
|
2445
|
+
this.onClose();
|
|
2446
|
+
}
|
|
2265
2447
|
};
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2448
|
+
var NewNodeModal = class extends Modal {
|
|
2449
|
+
fieldInputs = /* @__PURE__ */ new Map();
|
|
2450
|
+
typeSelect;
|
|
2451
|
+
submitCallback;
|
|
2452
|
+
fields;
|
|
2453
|
+
constructor(options) {
|
|
2454
|
+
super({
|
|
2455
|
+
title: "New Node",
|
|
2456
|
+
onClose: () => options.onCancel?.()
|
|
2457
|
+
});
|
|
2458
|
+
this.submitCallback = options.onSubmit;
|
|
2459
|
+
this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
|
|
2460
|
+
this.renderBody(options.nodeTypes);
|
|
2461
|
+
this.renderFooter();
|
|
2462
|
+
}
|
|
2463
|
+
renderBody(nodeTypes) {
|
|
2464
|
+
this.body.innerHTML = "";
|
|
2465
|
+
for (const [name, type] of this.fields) {
|
|
2466
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2467
|
+
const fieldGroup = this.renderField(name, label, type);
|
|
2468
|
+
this.body.appendChild(fieldGroup);
|
|
2277
2469
|
}
|
|
2278
|
-
if (
|
|
2279
|
-
|
|
2280
|
-
|
|
2470
|
+
if (nodeTypes && nodeTypes.length > 0) {
|
|
2471
|
+
const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2472
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
|
|
2473
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
2474
|
+
"select",
|
|
2475
|
+
{
|
|
2476
|
+
className: "g3p-modal-select",
|
|
2477
|
+
ref: (el) => this.typeSelect = el,
|
|
2478
|
+
children: [
|
|
2479
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Default" }),
|
|
2480
|
+
nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, children: type }))
|
|
2481
|
+
]
|
|
2482
|
+
}
|
|
2483
|
+
)
|
|
2484
|
+
] });
|
|
2485
|
+
this.body.appendChild(typeGroup);
|
|
2281
2486
|
}
|
|
2282
2487
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2488
|
+
renderField(name, label, type) {
|
|
2489
|
+
if (type === "boolean") {
|
|
2490
|
+
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: [
|
|
2491
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2492
|
+
"input",
|
|
2493
|
+
{
|
|
2494
|
+
type: "checkbox",
|
|
2495
|
+
className: "g3p-modal-checkbox",
|
|
2496
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2497
|
+
}
|
|
2498
|
+
),
|
|
2499
|
+
label
|
|
2500
|
+
] }) });
|
|
2501
|
+
}
|
|
2502
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2503
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
|
|
2504
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2505
|
+
"input",
|
|
2506
|
+
{
|
|
2507
|
+
type: type === "number" ? "number" : "text",
|
|
2508
|
+
className: "g3p-modal-input",
|
|
2509
|
+
placeholder: `Enter ${label.toLowerCase()}`,
|
|
2510
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2511
|
+
}
|
|
2512
|
+
)
|
|
2513
|
+
] });
|
|
2514
|
+
}
|
|
2515
|
+
renderFooter() {
|
|
2516
|
+
this.footer.innerHTML = "";
|
|
2517
|
+
this.footer.appendChild(
|
|
2518
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
|
|
2519
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2520
|
+
"button",
|
|
2521
|
+
{
|
|
2522
|
+
className: "g3p-modal-btn g3p-modal-btn-secondary",
|
|
2523
|
+
onClick: () => this.close(),
|
|
2524
|
+
children: "Cancel"
|
|
2525
|
+
}
|
|
2526
|
+
),
|
|
2527
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2528
|
+
"button",
|
|
2529
|
+
{
|
|
2530
|
+
className: "g3p-modal-btn g3p-modal-btn-primary",
|
|
2531
|
+
onClick: () => this.submit(),
|
|
2532
|
+
children: "Create"
|
|
2533
|
+
}
|
|
2534
|
+
)
|
|
2535
|
+
] })
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
submit() {
|
|
2539
|
+
const data = {};
|
|
2540
|
+
for (const [name, type] of this.fields) {
|
|
2541
|
+
const input = this.fieldInputs.get(name);
|
|
2542
|
+
if (!input) continue;
|
|
2543
|
+
if (type === "boolean") {
|
|
2544
|
+
data[name] = input.checked;
|
|
2545
|
+
} else if (type === "number") {
|
|
2546
|
+
const val = input.value;
|
|
2547
|
+
if (val) data[name] = Number(val);
|
|
2548
|
+
} else {
|
|
2549
|
+
const val = input.value.trim();
|
|
2550
|
+
if (val) data[name] = val;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (Object.keys(data).length === 0) {
|
|
2554
|
+
const firstInput = this.fieldInputs.values().next().value;
|
|
2555
|
+
if (firstInput) firstInput.focus();
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
if (this.typeSelect?.value) {
|
|
2559
|
+
data.type = this.typeSelect.value;
|
|
2560
|
+
}
|
|
2561
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2562
|
+
this.container.remove();
|
|
2563
|
+
this.submitCallback(data);
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
var EditNodeModal = class extends Modal {
|
|
2567
|
+
fieldInputs = /* @__PURE__ */ new Map();
|
|
2568
|
+
typeSelect;
|
|
2569
|
+
node;
|
|
2570
|
+
fields;
|
|
2571
|
+
submitCallback;
|
|
2572
|
+
deleteCallback;
|
|
2573
|
+
constructor(options) {
|
|
2574
|
+
super({
|
|
2575
|
+
title: "Edit Node",
|
|
2576
|
+
position: options.position,
|
|
2577
|
+
onClose: () => options.onCancel?.()
|
|
2578
|
+
});
|
|
2579
|
+
this.node = options.node;
|
|
2580
|
+
this.submitCallback = options.onSubmit;
|
|
2581
|
+
this.deleteCallback = options.onDelete;
|
|
2582
|
+
this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
|
|
2583
|
+
if (!options.fields && !this.node.title)
|
|
2584
|
+
this.node = { ...this.node, title: this.node.id };
|
|
2585
|
+
this.renderBody(options.nodeTypes);
|
|
2586
|
+
this.renderFooter();
|
|
2587
|
+
}
|
|
2588
|
+
renderBody(nodeTypes) {
|
|
2589
|
+
console.log(`renderBody`, this.node);
|
|
2590
|
+
this.body.innerHTML = "";
|
|
2591
|
+
for (const [name, type] of this.fields) {
|
|
2592
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2593
|
+
const currentValue = this.node[name];
|
|
2594
|
+
const fieldGroup = this.renderField(name, label, type, currentValue);
|
|
2595
|
+
this.body.appendChild(fieldGroup);
|
|
2596
|
+
}
|
|
2597
|
+
if (nodeTypes && nodeTypes.length > 0) {
|
|
2598
|
+
const currentType = this.node.type ?? "";
|
|
2599
|
+
const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2600
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
|
|
2601
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
2602
|
+
"select",
|
|
2603
|
+
{
|
|
2604
|
+
className: "g3p-modal-select",
|
|
2605
|
+
ref: (el) => this.typeSelect = el,
|
|
2606
|
+
children: [
|
|
2607
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
|
|
2608
|
+
nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
|
|
2609
|
+
]
|
|
2610
|
+
}
|
|
2611
|
+
)
|
|
2612
|
+
] });
|
|
2613
|
+
this.body.appendChild(typeGroup);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
renderField(name, label, type, currentValue) {
|
|
2617
|
+
if (type === "boolean") {
|
|
2618
|
+
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: [
|
|
2619
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2620
|
+
"input",
|
|
2621
|
+
{
|
|
2622
|
+
type: "checkbox",
|
|
2623
|
+
className: "g3p-modal-checkbox",
|
|
2624
|
+
checked: !!currentValue,
|
|
2625
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2626
|
+
}
|
|
2627
|
+
),
|
|
2628
|
+
label
|
|
2629
|
+
] }) });
|
|
2630
|
+
}
|
|
2631
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2632
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
|
|
2633
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2634
|
+
"input",
|
|
2635
|
+
{
|
|
2636
|
+
type: type === "number" ? "number" : "text",
|
|
2637
|
+
className: "g3p-modal-input",
|
|
2638
|
+
value: currentValue ?? "",
|
|
2639
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2640
|
+
}
|
|
2641
|
+
)
|
|
2642
|
+
] });
|
|
2643
|
+
}
|
|
2644
|
+
renderFooter() {
|
|
2645
|
+
this.footer.innerHTML = "";
|
|
2646
|
+
this.footer.appendChild(
|
|
2647
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
|
|
2648
|
+
this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2649
|
+
"button",
|
|
2650
|
+
{
|
|
2651
|
+
className: "g3p-modal-btn g3p-modal-btn-danger",
|
|
2652
|
+
onClick: () => this.delete(),
|
|
2653
|
+
children: "Delete"
|
|
2654
|
+
}
|
|
2655
|
+
),
|
|
2656
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
|
|
2657
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2658
|
+
"button",
|
|
2659
|
+
{
|
|
2660
|
+
className: "g3p-modal-btn g3p-modal-btn-secondary",
|
|
2661
|
+
onClick: () => this.close(),
|
|
2662
|
+
children: "Cancel"
|
|
2663
|
+
}
|
|
2664
|
+
),
|
|
2665
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2666
|
+
"button",
|
|
2667
|
+
{
|
|
2668
|
+
className: "g3p-modal-btn g3p-modal-btn-primary",
|
|
2669
|
+
onClick: () => this.submit(),
|
|
2670
|
+
children: "Save"
|
|
2671
|
+
}
|
|
2672
|
+
)
|
|
2673
|
+
] })
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
submit() {
|
|
2677
|
+
const data = { ...this.node };
|
|
2678
|
+
for (const [name, type] of this.fields) {
|
|
2679
|
+
const input = this.fieldInputs.get(name);
|
|
2680
|
+
if (!input) continue;
|
|
2681
|
+
if (type === "boolean") {
|
|
2682
|
+
data[name] = input.checked;
|
|
2683
|
+
} else if (type === "number") {
|
|
2684
|
+
const val = input.value;
|
|
2685
|
+
data[name] = val ? Number(val) : void 0;
|
|
2686
|
+
} else {
|
|
2687
|
+
const val = input.value.trim();
|
|
2688
|
+
data[name] = val || void 0;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (this.typeSelect) {
|
|
2692
|
+
data.type = this.typeSelect.value || void 0;
|
|
2693
|
+
}
|
|
2694
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2695
|
+
this.container.remove();
|
|
2696
|
+
this.submitCallback(data);
|
|
2697
|
+
}
|
|
2698
|
+
delete() {
|
|
2699
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2700
|
+
this.container.remove();
|
|
2701
|
+
this.deleteCallback?.();
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
var EditEdgeModal = class _EditEdgeModal extends Modal {
|
|
2705
|
+
sourceMarkerSelect;
|
|
2706
|
+
targetMarkerSelect;
|
|
2707
|
+
typeSelect;
|
|
2708
|
+
edge;
|
|
2709
|
+
submitCallback;
|
|
2710
|
+
deleteCallback;
|
|
2711
|
+
static markerTypes = ["none", "arrow", "circle", "diamond", "bar"];
|
|
2712
|
+
constructor(options) {
|
|
2713
|
+
super({
|
|
2714
|
+
title: "Edit Edge",
|
|
2715
|
+
onClose: () => options.onCancel?.()
|
|
2716
|
+
});
|
|
2717
|
+
this.edge = options.edge;
|
|
2718
|
+
this.submitCallback = options.onSubmit;
|
|
2719
|
+
this.deleteCallback = options.onDelete;
|
|
2720
|
+
this.renderBody(options.edgeTypes);
|
|
2721
|
+
this.renderFooter();
|
|
2722
|
+
}
|
|
2723
|
+
renderBody(edgeTypes) {
|
|
2724
|
+
this.body.innerHTML = "";
|
|
2725
|
+
const currentSourceMarker = this.edge.source?.marker ?? "none";
|
|
2726
|
+
const currentTargetMarker = this.edge.target?.marker ?? "arrow";
|
|
2727
|
+
const sourceGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2728
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Source Marker" }),
|
|
2729
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2730
|
+
"select",
|
|
2731
|
+
{
|
|
2732
|
+
className: "g3p-modal-select",
|
|
2733
|
+
ref: (el) => this.sourceMarkerSelect = el,
|
|
2734
|
+
children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentSourceMarker, children: type }))
|
|
2735
|
+
}
|
|
2736
|
+
)
|
|
2737
|
+
] });
|
|
2738
|
+
this.body.appendChild(sourceGroup);
|
|
2739
|
+
const targetGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2740
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Target Marker" }),
|
|
2741
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2742
|
+
"select",
|
|
2743
|
+
{
|
|
2744
|
+
className: "g3p-modal-select",
|
|
2745
|
+
ref: (el) => this.targetMarkerSelect = el,
|
|
2746
|
+
children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentTargetMarker, children: type }))
|
|
2747
|
+
}
|
|
2748
|
+
)
|
|
2749
|
+
] });
|
|
2750
|
+
this.body.appendChild(targetGroup);
|
|
2751
|
+
if (edgeTypes && edgeTypes.length > 0) {
|
|
2752
|
+
const currentType = this.edge.type ?? "";
|
|
2753
|
+
const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
|
|
2754
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
|
|
2755
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
2756
|
+
"select",
|
|
2757
|
+
{
|
|
2758
|
+
className: "g3p-modal-select",
|
|
2759
|
+
ref: (el) => this.typeSelect = el,
|
|
2760
|
+
children: [
|
|
2761
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
|
|
2762
|
+
edgeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
|
|
2763
|
+
]
|
|
2764
|
+
}
|
|
2765
|
+
)
|
|
2766
|
+
] });
|
|
2767
|
+
this.body.appendChild(typeGroup);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
renderFooter() {
|
|
2771
|
+
this.footer.innerHTML = "";
|
|
2772
|
+
this.footer.appendChild(
|
|
2773
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
|
|
2774
|
+
this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2775
|
+
"button",
|
|
2776
|
+
{
|
|
2777
|
+
className: "g3p-modal-btn g3p-modal-btn-danger",
|
|
2778
|
+
onClick: () => this.delete(),
|
|
2779
|
+
children: "Delete"
|
|
2780
|
+
}
|
|
2781
|
+
),
|
|
2782
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
|
|
2783
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2784
|
+
"button",
|
|
2785
|
+
{
|
|
2786
|
+
className: "g3p-modal-btn g3p-modal-btn-secondary",
|
|
2787
|
+
onClick: () => this.close(),
|
|
2788
|
+
children: "Cancel"
|
|
2789
|
+
}
|
|
2790
|
+
),
|
|
2791
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
2792
|
+
"button",
|
|
2793
|
+
{
|
|
2794
|
+
className: "g3p-modal-btn g3p-modal-btn-primary",
|
|
2795
|
+
onClick: () => this.submit(),
|
|
2796
|
+
children: "Save"
|
|
2797
|
+
}
|
|
2798
|
+
)
|
|
2799
|
+
] })
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
submit() {
|
|
2803
|
+
const data = {
|
|
2804
|
+
...this.edge,
|
|
2805
|
+
source: {
|
|
2806
|
+
...this.edge.source,
|
|
2807
|
+
marker: this.sourceMarkerSelect.value === "none" ? void 0 : this.sourceMarkerSelect.value
|
|
2808
|
+
},
|
|
2809
|
+
target: {
|
|
2810
|
+
...this.edge.target,
|
|
2811
|
+
marker: this.targetMarkerSelect.value === "none" ? void 0 : this.targetMarkerSelect.value
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
if (this.typeSelect) {
|
|
2815
|
+
data.type = this.typeSelect.value || void 0;
|
|
2816
|
+
}
|
|
2817
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2818
|
+
this.container.remove();
|
|
2819
|
+
this.submitCallback(data);
|
|
2820
|
+
}
|
|
2821
|
+
delete() {
|
|
2822
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2823
|
+
this.container.remove();
|
|
2824
|
+
this.deleteCallback?.();
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
|
|
2828
|
+
// src/canvas/canvas.tsx
|
|
2829
|
+
var import_styles = __toESM(require("./styles.css?raw"), 1);
|
|
2830
|
+
var import_jsx_runtime6 = require("jsx-dom/jsx-runtime");
|
|
2831
|
+
var log10 = logger("canvas");
|
|
2286
2832
|
var Canvas = class {
|
|
2287
2833
|
container;
|
|
2288
2834
|
root;
|
|
@@ -2295,19 +2841,26 @@ var Canvas = class {
|
|
|
2295
2841
|
curSegs;
|
|
2296
2842
|
updating;
|
|
2297
2843
|
// Pan-zoom state
|
|
2298
|
-
isPanning = false;
|
|
2299
|
-
panStart = null;
|
|
2300
|
-
transformStart = null;
|
|
2301
2844
|
panScale = null;
|
|
2302
2845
|
zoomControls;
|
|
2303
|
-
|
|
2846
|
+
// Edit mode state machine
|
|
2847
|
+
editMode;
|
|
2848
|
+
api;
|
|
2849
|
+
// New-edge visual element
|
|
2850
|
+
newEdgeEl;
|
|
2851
|
+
// Pending drag state (for double-click debounce)
|
|
2852
|
+
pendingDrag = null;
|
|
2853
|
+
constructor(api, options) {
|
|
2304
2854
|
Object.assign(this, options);
|
|
2855
|
+
this.api = api;
|
|
2305
2856
|
this.allNodes = /* @__PURE__ */ new Map();
|
|
2306
2857
|
this.curNodes = /* @__PURE__ */ new Map();
|
|
2307
2858
|
this.curSegs = /* @__PURE__ */ new Map();
|
|
2308
2859
|
this.updating = false;
|
|
2309
2860
|
this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
|
|
2310
2861
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
2862
|
+
this.editMode = new EditMode();
|
|
2863
|
+
this.editMode.editable = this.editable;
|
|
2311
2864
|
this.createMeasurementContainer();
|
|
2312
2865
|
this.createCanvasContainer();
|
|
2313
2866
|
if (this.panZoom) this.setupPanZoom();
|
|
@@ -2352,7 +2905,6 @@ var Canvas = class {
|
|
|
2352
2905
|
if (gnode.isDummy) {
|
|
2353
2906
|
node = new Node2(this, gnode, true);
|
|
2354
2907
|
node.renderContainer();
|
|
2355
|
-
node.setPos(gnode.pos);
|
|
2356
2908
|
this.allNodes.set(key, node);
|
|
2357
2909
|
} else {
|
|
2358
2910
|
if (!this.allNodes.has(key))
|
|
@@ -2361,6 +2913,7 @@ var Canvas = class {
|
|
|
2361
2913
|
}
|
|
2362
2914
|
this.curNodes.set(gnode.id, node);
|
|
2363
2915
|
node.append();
|
|
2916
|
+
node.setPos(gnode.pos);
|
|
2364
2917
|
}
|
|
2365
2918
|
updateNode(gnode) {
|
|
2366
2919
|
if (gnode.isDummy) throw new Error("dummy node cannot be updated");
|
|
@@ -2382,10 +2935,10 @@ var Canvas = class {
|
|
|
2382
2935
|
this.curSegs.set(gseg.id, seg);
|
|
2383
2936
|
seg.append();
|
|
2384
2937
|
}
|
|
2385
|
-
updateSeg(gseg) {
|
|
2938
|
+
updateSeg(gseg, g) {
|
|
2386
2939
|
const seg = this.curSegs.get(gseg.id);
|
|
2387
2940
|
if (!seg) throw new Error("seg not found");
|
|
2388
|
-
seg.update(gseg);
|
|
2941
|
+
seg.update(gseg, g);
|
|
2389
2942
|
}
|
|
2390
2943
|
deleteSeg(gseg) {
|
|
2391
2944
|
const seg = this.curSegs.get(gseg.id);
|
|
@@ -2405,24 +2958,88 @@ var Canvas = class {
|
|
|
2405
2958
|
for (const node of newNodes.values()) {
|
|
2406
2959
|
node.measure(isVertical);
|
|
2407
2960
|
const { id, version } = node.data;
|
|
2408
|
-
const key =
|
|
2961
|
+
const key = `k:${id}:${version}`;
|
|
2409
2962
|
this.allNodes.set(key, node);
|
|
2410
2963
|
node.renderContainer();
|
|
2411
2964
|
}
|
|
2412
2965
|
this.measurement.innerHTML = "";
|
|
2413
2966
|
return newNodes;
|
|
2414
2967
|
}
|
|
2968
|
+
// ========== Mouse event handlers ==========
|
|
2415
2969
|
onClick(e) {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2970
|
+
const hit = this.hitTest(e.clientX, e.clientY);
|
|
2971
|
+
if (hit.type === "node") {
|
|
2972
|
+
this.api.handleClickNode(hit.node.data.id);
|
|
2973
|
+
} else if (hit.type === "edge") {
|
|
2974
|
+
this.api.handleClickEdge(hit.segId);
|
|
2975
|
+
}
|
|
2420
2976
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2977
|
+
onDoubleClick(e) {
|
|
2978
|
+
if (this.pendingDrag) {
|
|
2979
|
+
window.clearTimeout(this.pendingDrag.timeout);
|
|
2980
|
+
this.pendingDrag = null;
|
|
2981
|
+
}
|
|
2982
|
+
if (!this.editMode.editable) return;
|
|
2983
|
+
const hit = this.hitTest(e.clientX, e.clientY);
|
|
2984
|
+
if (hit.type === "node") {
|
|
2985
|
+
if (hit.node.isDummy) return;
|
|
2986
|
+
this.api.handleEditNode(hit.node.data.id);
|
|
2987
|
+
} else if (hit.type === "edge") {
|
|
2988
|
+
this.api.handleEditEdge(hit.segId);
|
|
2989
|
+
} else {
|
|
2990
|
+
this.api.handleNewNode();
|
|
2991
|
+
}
|
|
2423
2992
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2993
|
+
// ========== Built-in Modals ==========
|
|
2994
|
+
/** Show the new node modal */
|
|
2995
|
+
showNewNodeModal(callback) {
|
|
2996
|
+
const nodeTypes = Object.keys(this.nodeTypes);
|
|
2997
|
+
const fields = this.api.getNodeFields();
|
|
2998
|
+
const modal = new NewNodeModal({
|
|
2999
|
+
nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
|
|
3000
|
+
fields: fields.size > 0 ? fields : void 0,
|
|
3001
|
+
onSubmit: (data) => {
|
|
3002
|
+
callback(data);
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
modal.show(document.body);
|
|
3006
|
+
}
|
|
3007
|
+
/** Show the edit node modal */
|
|
3008
|
+
showEditNodeModal(node, callback) {
|
|
3009
|
+
const nodeTypes = Object.keys(this.nodeTypes);
|
|
3010
|
+
const fields = this.api.getNodeFields();
|
|
3011
|
+
const modal = new EditNodeModal({
|
|
3012
|
+
node,
|
|
3013
|
+
nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
|
|
3014
|
+
fields: fields.size > 0 ? fields : void 0,
|
|
3015
|
+
onSubmit: (data) => {
|
|
3016
|
+
callback(data);
|
|
3017
|
+
},
|
|
3018
|
+
onDelete: () => {
|
|
3019
|
+
this.api.handleDeleteNode(node.id);
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
3022
|
+
modal.show(document.body);
|
|
3023
|
+
}
|
|
3024
|
+
/** Show the edit edge modal */
|
|
3025
|
+
showEditEdgeModal(edge, callback) {
|
|
3026
|
+
const modal = new EditEdgeModal({
|
|
3027
|
+
edge,
|
|
3028
|
+
edgeTypes: Object.keys(this.edgeTypes),
|
|
3029
|
+
onSubmit: callback,
|
|
3030
|
+
onDelete: () => {
|
|
3031
|
+
this.api.handleDeleteEdge(edge.id);
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
modal.show(document.body);
|
|
3035
|
+
}
|
|
3036
|
+
onContextMenu(e) {
|
|
3037
|
+
}
|
|
3038
|
+
groupTransform() {
|
|
3039
|
+
return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
|
|
3040
|
+
}
|
|
3041
|
+
viewBox() {
|
|
3042
|
+
const p = this.padding;
|
|
2426
3043
|
const x = this.bounds.min.x - p;
|
|
2427
3044
|
const y = this.bounds.min.y - p;
|
|
2428
3045
|
const w = this.bounds.max.x - this.bounds.min.x + p * 2;
|
|
@@ -2431,53 +3048,52 @@ var Canvas = class {
|
|
|
2431
3048
|
}
|
|
2432
3049
|
generateDynamicStyles() {
|
|
2433
3050
|
let css = "";
|
|
2434
|
-
|
|
2435
|
-
css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
|
|
3051
|
+
css += themeToCSS(this.theme, `.g3p-canvas-container`);
|
|
2436
3052
|
for (const [type, vars] of Object.entries(this.nodeTypes)) {
|
|
2437
|
-
css += themeToCSS(vars,
|
|
3053
|
+
css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
|
|
2438
3054
|
}
|
|
2439
3055
|
for (const [type, vars] of Object.entries(this.edgeTypes)) {
|
|
2440
|
-
css += themeToCSS(vars,
|
|
3056
|
+
css += themeToCSS(vars, `.g3p-edge-type-${type}`);
|
|
2441
3057
|
}
|
|
2442
3058
|
return css;
|
|
2443
3059
|
}
|
|
2444
3060
|
createCanvasContainer() {
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
3061
|
+
if (!document.getElementById("g3p-styles")) {
|
|
3062
|
+
const baseStyleEl = document.createElement("style");
|
|
3063
|
+
baseStyleEl.id = "g3p-styles";
|
|
3064
|
+
baseStyleEl.textContent = import_styles.default;
|
|
3065
|
+
document.head.appendChild(baseStyleEl);
|
|
3066
|
+
}
|
|
2451
3067
|
const dynamicStyles = this.generateDynamicStyles();
|
|
2452
3068
|
if (dynamicStyles) {
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
document.head.appendChild(
|
|
3069
|
+
const dynamicStyleEl = document.createElement("style");
|
|
3070
|
+
dynamicStyleEl.textContent = dynamicStyles;
|
|
3071
|
+
document.head.appendChild(dynamicStyleEl);
|
|
2456
3072
|
}
|
|
2457
|
-
const c = styler("canvas", import_canvas.default, this.classPrefix);
|
|
2458
3073
|
const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
|
|
2459
|
-
this.container = /* @__PURE__ */ (0,
|
|
3074
|
+
this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2460
3075
|
"div",
|
|
2461
3076
|
{
|
|
2462
|
-
className:
|
|
3077
|
+
className: `g3p-canvas-container ${colorModeClass}`.trim(),
|
|
2463
3078
|
ref: (el) => this.container = el,
|
|
2464
3079
|
onContextMenu: this.onContextMenu.bind(this),
|
|
2465
|
-
children: /* @__PURE__ */ (0,
|
|
3080
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
2466
3081
|
"svg",
|
|
2467
3082
|
{
|
|
2468
3083
|
ref: (el) => this.root = el,
|
|
2469
|
-
className:
|
|
3084
|
+
className: "g3p-canvas-root",
|
|
2470
3085
|
width: this.width,
|
|
2471
3086
|
height: this.height,
|
|
2472
3087
|
viewBox: this.viewBox(),
|
|
2473
3088
|
preserveAspectRatio: "xMidYMid meet",
|
|
2474
3089
|
onClick: this.onClick.bind(this),
|
|
3090
|
+
onDblClick: this.onDoubleClick.bind(this),
|
|
2475
3091
|
children: [
|
|
2476
|
-
/* @__PURE__ */ (0,
|
|
2477
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize,
|
|
2478
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize,
|
|
3092
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
|
|
3093
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
|
|
3094
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
|
|
2479
3095
|
] }),
|
|
2480
|
-
/* @__PURE__ */ (0,
|
|
3096
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2481
3097
|
"g",
|
|
2482
3098
|
{
|
|
2483
3099
|
ref: (el) => this.group = el,
|
|
@@ -2496,14 +3112,34 @@ var Canvas = class {
|
|
|
2496
3112
|
this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
|
|
2497
3113
|
document.addEventListener("mousemove", this.onMouseMove.bind(this));
|
|
2498
3114
|
document.addEventListener("mouseup", this.onMouseUp.bind(this));
|
|
3115
|
+
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
|
2499
3116
|
this.createZoomControls();
|
|
2500
3117
|
}
|
|
3118
|
+
onKeyDown(e) {
|
|
3119
|
+
if (e.key === "Escape" && this.editMode.isCreatingEdge) {
|
|
3120
|
+
this.endNewEdge(true);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
2501
3123
|
/** Convert screen coordinates to canvas-relative coordinates */
|
|
2502
3124
|
screenToCanvas(screen) {
|
|
2503
3125
|
const rect = this.container.getBoundingClientRect();
|
|
2504
3126
|
return canvasPos(screen.x - rect.left, screen.y - rect.top);
|
|
2505
3127
|
}
|
|
2506
|
-
/**
|
|
3128
|
+
/** Convert canvas coordinates to graph coordinates */
|
|
3129
|
+
canvasToGraph(canvas) {
|
|
3130
|
+
const vb = this.currentViewBox();
|
|
3131
|
+
const { scale, offsetX, offsetY } = this.getEffectiveScale();
|
|
3132
|
+
return graphPos(
|
|
3133
|
+
vb.x - offsetX + canvas.x * scale,
|
|
3134
|
+
vb.y - offsetY + canvas.y * scale
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
/** Convert screen coordinates to graph coordinates */
|
|
3138
|
+
screenToGraph(screen) {
|
|
3139
|
+
const canvas = this.screenToCanvas(screen);
|
|
3140
|
+
return this.canvasToGraph(canvas);
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
2507
3143
|
* Get the effective scale from canvas pixels to graph units,
|
|
2508
3144
|
* accounting for preserveAspectRatio="xMidYMid meet" which uses
|
|
2509
3145
|
* the smaller scale (to fit) and centers the content.
|
|
@@ -2520,15 +3156,6 @@ var Canvas = class {
|
|
|
2520
3156
|
const offsetY = (actualH - vb.h) / 2;
|
|
2521
3157
|
return { scale, offsetX, offsetY };
|
|
2522
3158
|
}
|
|
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
3159
|
/** Get current viewBox as an object */
|
|
2533
3160
|
currentViewBox() {
|
|
2534
3161
|
const p = this.padding;
|
|
@@ -2563,28 +3190,67 @@ var Canvas = class {
|
|
|
2563
3190
|
onMouseDown(e) {
|
|
2564
3191
|
if (e.button !== 0) return;
|
|
2565
3192
|
if (e.target.closest(".g3p-zoom-controls")) return;
|
|
2566
|
-
this.
|
|
2567
|
-
this.
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
3193
|
+
const hit = this.hitTest(e.clientX, e.clientY);
|
|
3194
|
+
if (this.editMode.editable && (hit.type === "node" || hit.type === "port")) {
|
|
3195
|
+
const node = hit.node;
|
|
3196
|
+
if (node.isDummy) return;
|
|
3197
|
+
e.preventDefault();
|
|
3198
|
+
e.stopPropagation();
|
|
3199
|
+
const startGraph = this.screenToGraph(hit.center);
|
|
3200
|
+
const portId = hit.type === "port" ? hit.port : void 0;
|
|
3201
|
+
this.pendingDrag = {
|
|
3202
|
+
timeout: window.setTimeout(() => {
|
|
3203
|
+
if (this.pendingDrag) {
|
|
3204
|
+
this.startNewEdge(this.pendingDrag.nodeId, this.pendingDrag.startGraph, this.pendingDrag.portId);
|
|
3205
|
+
this.pendingDrag = null;
|
|
3206
|
+
}
|
|
3207
|
+
}, 200),
|
|
3208
|
+
nodeId: node.data.id,
|
|
3209
|
+
startGraph,
|
|
3210
|
+
portId
|
|
3211
|
+
};
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
if (hit.type === "canvas" || hit.type === "edge") {
|
|
3215
|
+
const startCanvas = this.screenToCanvas(screenPos(e.clientX, e.clientY));
|
|
3216
|
+
this.editMode.startPan(startCanvas, { ...this.transform });
|
|
3217
|
+
const { scale } = this.getEffectiveScale();
|
|
3218
|
+
this.panScale = { x: scale, y: scale };
|
|
3219
|
+
this.container.style.cursor = "grabbing";
|
|
3220
|
+
e.preventDefault();
|
|
3221
|
+
}
|
|
2573
3222
|
}
|
|
2574
3223
|
onMouseMove(e) {
|
|
2575
|
-
if (
|
|
3224
|
+
if (this.editMode.isCreatingEdge) {
|
|
3225
|
+
const screenCursor = screenPos(e.clientX, e.clientY);
|
|
3226
|
+
const canvasCursor = this.screenToCanvas(screenCursor);
|
|
3227
|
+
const graphCursor = this.canvasToGraph(canvasCursor);
|
|
3228
|
+
this.editMode.updateNewEdgePosition(graphCursor);
|
|
3229
|
+
this.updateNewEdgeVisual();
|
|
3230
|
+
this.detectHoverTarget(e.clientX, e.clientY);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
if (!this.editMode.isPanning || !this.panScale) return;
|
|
3234
|
+
const panState = this.editMode.state;
|
|
3235
|
+
if (panState.type !== "panning") return;
|
|
2576
3236
|
const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
|
|
2577
|
-
const dx = current.x -
|
|
2578
|
-
const dy = current.y -
|
|
2579
|
-
this.transform.x =
|
|
2580
|
-
this.transform.y =
|
|
3237
|
+
const dx = current.x - panState.startCanvas.x;
|
|
3238
|
+
const dy = current.y - panState.startCanvas.y;
|
|
3239
|
+
this.transform.x = panState.startTransform.x + dx * this.panScale.x;
|
|
3240
|
+
this.transform.y = panState.startTransform.y + dy * this.panScale.y;
|
|
2581
3241
|
this.applyTransform();
|
|
2582
3242
|
}
|
|
2583
3243
|
onMouseUp(e) {
|
|
2584
|
-
if (
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
3244
|
+
if (this.pendingDrag) {
|
|
3245
|
+
window.clearTimeout(this.pendingDrag.timeout);
|
|
3246
|
+
this.pendingDrag = null;
|
|
3247
|
+
}
|
|
3248
|
+
if (this.editMode.isCreatingEdge) {
|
|
3249
|
+
this.endNewEdge(false);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
if (!this.editMode.isPanning) return;
|
|
3253
|
+
this.editMode.reset();
|
|
2588
3254
|
this.panScale = null;
|
|
2589
3255
|
this.container.style.cursor = "";
|
|
2590
3256
|
}
|
|
@@ -2594,12 +3260,11 @@ var Canvas = class {
|
|
|
2594
3260
|
this.updateZoomLevel();
|
|
2595
3261
|
}
|
|
2596
3262
|
createZoomControls() {
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
/* @__PURE__ */ (0,
|
|
2600
|
-
/* @__PURE__ */ (0,
|
|
2601
|
-
/* @__PURE__ */ (0,
|
|
2602
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
|
|
3263
|
+
this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "g3p-zoom-controls", children: [
|
|
3264
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomIn(), children: "+" }),
|
|
3265
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "g3p-zoom-level", id: "g3p-zoom-level", children: "100%" }),
|
|
3266
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomOut(), children: "\u2212" }),
|
|
3267
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn g3p-zoom-reset", onClick: () => this.zoomReset(), children: "\u27F2" })
|
|
2603
3268
|
] });
|
|
2604
3269
|
this.container.appendChild(this.zoomControls);
|
|
2605
3270
|
}
|
|
@@ -2621,17 +3286,215 @@ var Canvas = class {
|
|
|
2621
3286
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
2622
3287
|
this.applyTransform();
|
|
2623
3288
|
}
|
|
3289
|
+
// ==================== New-Edge Mode ====================
|
|
3290
|
+
/** Start creating a new edge from a node */
|
|
3291
|
+
startNewEdge(sourceNodeId, startGraph, sourcePortId) {
|
|
3292
|
+
this.editMode.startNewEdge(sourceNodeId, startGraph, sourcePortId);
|
|
3293
|
+
this.updateNewEdgeVisual();
|
|
3294
|
+
this.container.style.cursor = "crosshair";
|
|
3295
|
+
}
|
|
3296
|
+
/** Update the new-edge visual during drag */
|
|
3297
|
+
updateNewEdgeVisual() {
|
|
3298
|
+
const state = this.editMode.getNewEdgeState();
|
|
3299
|
+
if (!state) {
|
|
3300
|
+
this.removeNewEdgeVisual();
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
if (this.newEdgeEl) {
|
|
3304
|
+
this.newEdgeEl.remove();
|
|
3305
|
+
}
|
|
3306
|
+
this.newEdgeEl = renderNewEdge({
|
|
3307
|
+
start: state.startGraph,
|
|
3308
|
+
end: state.currentGraph
|
|
3309
|
+
});
|
|
3310
|
+
this.group.appendChild(this.newEdgeEl);
|
|
3311
|
+
}
|
|
3312
|
+
/** Remove the new-edge visual */
|
|
3313
|
+
removeNewEdgeVisual() {
|
|
3314
|
+
if (this.newEdgeEl) {
|
|
3315
|
+
this.newEdgeEl.remove();
|
|
3316
|
+
this.newEdgeEl = void 0;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
/** Complete or cancel the new-edge creation */
|
|
3320
|
+
endNewEdge(cancelled = false) {
|
|
3321
|
+
const state = this.editMode.getNewEdgeState();
|
|
3322
|
+
if (!state) return;
|
|
3323
|
+
if (!cancelled) {
|
|
3324
|
+
const { target, source } = state;
|
|
3325
|
+
if (target?.type == "node") {
|
|
3326
|
+
this.api.handleAddEdge({ id: "", source, target });
|
|
3327
|
+
} else {
|
|
3328
|
+
this.api.handleNewNodeFrom(source);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
this.removeNewEdgeVisual();
|
|
3332
|
+
this.clearDropTargetHighlight();
|
|
3333
|
+
this.editMode.reset();
|
|
3334
|
+
this.container.style.cursor = "";
|
|
3335
|
+
}
|
|
3336
|
+
/** Find node data by internal ID */
|
|
3337
|
+
findNodeDataById(nodeId) {
|
|
3338
|
+
for (const node of this.curNodes.values()) {
|
|
3339
|
+
if (node.data?.id === nodeId) {
|
|
3340
|
+
return node.data.data;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return null;
|
|
3344
|
+
}
|
|
3345
|
+
/** Set hover target for new-edge mode */
|
|
3346
|
+
setNewEdgeHoverTarget(id, port) {
|
|
3347
|
+
this.clearDropTargetHighlight();
|
|
3348
|
+
this.editMode.setHoverTarget({ type: "node", id, port });
|
|
3349
|
+
if (port) {
|
|
3350
|
+
const portEl = this.container?.querySelector(`.g3p-node-port[data-node-id="${id}"][data-port-id="${port}"]`);
|
|
3351
|
+
if (portEl) portEl.classList.add("g3p-drop-target");
|
|
3352
|
+
} else {
|
|
3353
|
+
const node = this.curNodes.get(id);
|
|
3354
|
+
if (node?.container) node.container.classList.add("g3p-drop-target");
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
/** Clear hover target for new-edge mode */
|
|
3358
|
+
clearNewEdgeHoverTarget() {
|
|
3359
|
+
this.clearDropTargetHighlight();
|
|
3360
|
+
this.editMode.setHoverTarget(null);
|
|
3361
|
+
}
|
|
3362
|
+
/** Remove drop target highlight from all elements */
|
|
3363
|
+
clearDropTargetHighlight() {
|
|
3364
|
+
for (const node of this.curNodes.values()) {
|
|
3365
|
+
node.container?.classList.remove("g3p-drop-target");
|
|
3366
|
+
}
|
|
3367
|
+
this.container?.querySelectorAll(".g3p-drop-target").forEach((el) => {
|
|
3368
|
+
el.classList.remove("g3p-drop-target");
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
/** Detect hover target during new-edge drag using elementFromPoint */
|
|
3372
|
+
detectHoverTarget(clientX, clientY) {
|
|
3373
|
+
if (this.newEdgeEl) {
|
|
3374
|
+
this.newEdgeEl.style.display = "none";
|
|
3375
|
+
}
|
|
3376
|
+
const el = document.elementFromPoint(clientX, clientY);
|
|
3377
|
+
if (this.newEdgeEl) {
|
|
3378
|
+
this.newEdgeEl.style.display = "";
|
|
3379
|
+
}
|
|
3380
|
+
if (!el) {
|
|
3381
|
+
this.clearNewEdgeHoverTarget();
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const portEl = el.closest(".g3p-node-port");
|
|
3385
|
+
if (portEl) {
|
|
3386
|
+
const nodeId = portEl.getAttribute("data-node-id");
|
|
3387
|
+
const portId = portEl.getAttribute("data-port-id");
|
|
3388
|
+
if (nodeId && portId) {
|
|
3389
|
+
const node = this.curNodes.get(nodeId);
|
|
3390
|
+
if (node && !node.isDummy) {
|
|
3391
|
+
this.setNewEdgeHoverTarget(nodeId, portId);
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
const nodeEl = el.closest(".g3p-node-container");
|
|
3397
|
+
if (nodeEl) {
|
|
3398
|
+
const nodeId = nodeEl.getAttribute("data-node-id");
|
|
3399
|
+
if (nodeId) {
|
|
3400
|
+
const node = this.curNodes.get(nodeId);
|
|
3401
|
+
if (node && !node.isDummy) {
|
|
3402
|
+
this.setNewEdgeHoverTarget(node.data.id);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
this.clearNewEdgeHoverTarget();
|
|
3408
|
+
}
|
|
3409
|
+
// ==================== Hit Testing ====================
|
|
3410
|
+
/** Result of a hit test */
|
|
3411
|
+
hitTest(clientX, clientY) {
|
|
3412
|
+
const el = document.elementFromPoint(clientX, clientY);
|
|
3413
|
+
if (!el) return { type: "canvas" };
|
|
3414
|
+
const getCenter = (el2) => {
|
|
3415
|
+
const rect = el2.getBoundingClientRect();
|
|
3416
|
+
return screenPos(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
3417
|
+
};
|
|
3418
|
+
const portEl = el.closest(".g3p-node-port");
|
|
3419
|
+
if (portEl) {
|
|
3420
|
+
const nodeId = portEl.getAttribute("data-node-id");
|
|
3421
|
+
const portId = portEl.getAttribute("data-port-id");
|
|
3422
|
+
if (nodeId && portId) {
|
|
3423
|
+
const center = getCenter(portEl);
|
|
3424
|
+
const node = this.curNodes.get(nodeId);
|
|
3425
|
+
if (node) {
|
|
3426
|
+
return { type: "port", node, port: portId, center };
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
const nodeEl = el.closest(".g3p-node-container");
|
|
3431
|
+
if (nodeEl) {
|
|
3432
|
+
const nodeId = nodeEl.getAttribute("data-node-id");
|
|
3433
|
+
if (nodeId) {
|
|
3434
|
+
const borderEl = el.closest(".g3p-node-border");
|
|
3435
|
+
const center = getCenter(borderEl ?? nodeEl);
|
|
3436
|
+
const node = this.curNodes.get(nodeId);
|
|
3437
|
+
if (node) {
|
|
3438
|
+
return { type: "node", node, center };
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
const edgeEl = el.closest(".g3p-seg-container");
|
|
3443
|
+
if (edgeEl) {
|
|
3444
|
+
const segId = edgeEl.getAttribute("data-edge-id");
|
|
3445
|
+
if (segId) {
|
|
3446
|
+
return { type: "edge", segId };
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return { type: "canvas" };
|
|
3450
|
+
}
|
|
2624
3451
|
};
|
|
3452
|
+
var themeVarMap = {
|
|
3453
|
+
// Canvas
|
|
3454
|
+
bg: "--g3p-bg",
|
|
3455
|
+
shadow: "--g3p-shadow",
|
|
3456
|
+
// Node
|
|
3457
|
+
border: "--g3p-border",
|
|
3458
|
+
borderHover: "--g3p-border-hover",
|
|
3459
|
+
borderSelected: "--g3p-border-selected",
|
|
3460
|
+
text: "--g3p-text",
|
|
3461
|
+
textMuted: "--g3p-text-muted",
|
|
3462
|
+
// Port
|
|
3463
|
+
bgHover: "--g3p-port-bg-hover",
|
|
3464
|
+
// Edge
|
|
3465
|
+
color: "--g3p-edge-color"
|
|
3466
|
+
};
|
|
3467
|
+
function themeToCSS(theme, selector, prefix = "") {
|
|
3468
|
+
const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
|
|
3469
|
+
if (!entries.length) return "";
|
|
3470
|
+
let css = `${selector} {
|
|
3471
|
+
`;
|
|
3472
|
+
for (const [key, value] of entries) {
|
|
3473
|
+
let cssVar = themeVarMap[key];
|
|
3474
|
+
if (key === "bg" && prefix === "node") {
|
|
3475
|
+
cssVar = "--g3p-bg-node";
|
|
3476
|
+
} else if (key === "bg" && prefix === "port") {
|
|
3477
|
+
cssVar = "--g3p-port-bg";
|
|
3478
|
+
}
|
|
3479
|
+
if (cssVar) {
|
|
3480
|
+
css += ` ${cssVar}: ${value};
|
|
3481
|
+
`;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
css += "}\n";
|
|
3485
|
+
return css;
|
|
3486
|
+
}
|
|
2625
3487
|
|
|
2626
3488
|
// src/canvas/render-node.tsx
|
|
2627
|
-
var
|
|
2628
|
-
function renderNode(node) {
|
|
3489
|
+
var import_jsx_runtime7 = require("jsx-dom/jsx-runtime");
|
|
3490
|
+
function renderNode(node, props) {
|
|
2629
3491
|
if (typeof node == "string") node = { id: node };
|
|
2630
|
-
const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
|
|
3492
|
+
const title = node?.title ?? props?.title ?? node?.label ?? node?.name ?? node?.text ?? props?.text ?? node?.id ?? "?";
|
|
2631
3493
|
const detail = node?.detail ?? node?.description ?? node?.subtitle;
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3494
|
+
console.log(`renderNode: ${node.id} ${title} ${detail}`);
|
|
3495
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "g3p-node-default", children: [
|
|
3496
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-title", children: title }),
|
|
3497
|
+
detail && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-detail", children: detail })
|
|
2635
3498
|
] });
|
|
2636
3499
|
}
|
|
2637
3500
|
|
|
@@ -2663,7 +3526,6 @@ function defaults() {
|
|
|
2663
3526
|
},
|
|
2664
3527
|
canvas: {
|
|
2665
3528
|
renderNode,
|
|
2666
|
-
classPrefix: "g3p",
|
|
2667
3529
|
width: "100%",
|
|
2668
3530
|
height: "100%",
|
|
2669
3531
|
padding: 20,
|
|
@@ -2700,26 +3562,50 @@ var Updater = class _Updater {
|
|
|
2700
3562
|
this.update.addNodes.push(node);
|
|
2701
3563
|
return this;
|
|
2702
3564
|
}
|
|
3565
|
+
addNodes(...nodes) {
|
|
3566
|
+
this.update.addNodes.push(...nodes);
|
|
3567
|
+
return this;
|
|
3568
|
+
}
|
|
2703
3569
|
deleteNode(node) {
|
|
2704
3570
|
this.update.removeNodes.push(node);
|
|
2705
3571
|
return this;
|
|
2706
3572
|
}
|
|
3573
|
+
deleteNodes(...nodes) {
|
|
3574
|
+
this.update.removeNodes.push(...nodes);
|
|
3575
|
+
return this;
|
|
3576
|
+
}
|
|
2707
3577
|
updateNode(node) {
|
|
2708
3578
|
this.update.updateNodes.push(node);
|
|
2709
3579
|
return this;
|
|
2710
3580
|
}
|
|
3581
|
+
updateNodes(...nodes) {
|
|
3582
|
+
this.update.updateNodes.push(...nodes);
|
|
3583
|
+
return this;
|
|
3584
|
+
}
|
|
2711
3585
|
addEdge(edge) {
|
|
2712
3586
|
this.update.addEdges.push(edge);
|
|
2713
3587
|
return this;
|
|
2714
3588
|
}
|
|
3589
|
+
addEdges(...edges) {
|
|
3590
|
+
this.update.addEdges.push(...edges);
|
|
3591
|
+
return this;
|
|
3592
|
+
}
|
|
2715
3593
|
deleteEdge(edge) {
|
|
2716
3594
|
this.update.removeEdges.push(edge);
|
|
2717
3595
|
return this;
|
|
2718
3596
|
}
|
|
3597
|
+
deleteEdges(...edges) {
|
|
3598
|
+
this.update.removeEdges.push(...edges);
|
|
3599
|
+
return this;
|
|
3600
|
+
}
|
|
2719
3601
|
updateEdge(edge) {
|
|
2720
3602
|
this.update.updateEdges.push(edge);
|
|
2721
3603
|
return this;
|
|
2722
3604
|
}
|
|
3605
|
+
updateEdges(...edges) {
|
|
3606
|
+
this.update.updateEdges.push(...edges);
|
|
3607
|
+
return this;
|
|
3608
|
+
}
|
|
2723
3609
|
static add(nodes, edges) {
|
|
2724
3610
|
const updater = new _Updater();
|
|
2725
3611
|
updater.update.addNodes = nodes;
|
|
@@ -2740,22 +3626,19 @@ var API = class {
|
|
|
2740
3626
|
nodeIds;
|
|
2741
3627
|
edgeIds;
|
|
2742
3628
|
nodeVersions;
|
|
3629
|
+
nodeOverrides;
|
|
3630
|
+
edgeOverrides;
|
|
3631
|
+
nodeFields;
|
|
2743
3632
|
nextNodeId;
|
|
2744
3633
|
nextEdgeId;
|
|
3634
|
+
events;
|
|
2745
3635
|
root;
|
|
2746
3636
|
constructor(args) {
|
|
2747
3637
|
this.root = args.root;
|
|
2748
3638
|
this.options = applyDefaults(args.options);
|
|
2749
|
-
|
|
2750
|
-
this.
|
|
2751
|
-
this.
|
|
2752
|
-
this.index = 0;
|
|
2753
|
-
this.nodeIds = /* @__PURE__ */ new Map();
|
|
2754
|
-
this.edgeIds = /* @__PURE__ */ new Map();
|
|
2755
|
-
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
2756
|
-
this.nextNodeId = 1;
|
|
2757
|
-
this.nextEdgeId = 1;
|
|
2758
|
-
this.canvas = new Canvas({
|
|
3639
|
+
this.events = args.events || {};
|
|
3640
|
+
this.reset();
|
|
3641
|
+
this.canvas = new Canvas(this, {
|
|
2759
3642
|
...this.options.canvas,
|
|
2760
3643
|
dummyNodeSize: this.options.graph.dummyNodeSize,
|
|
2761
3644
|
orientation: this.options.graph.orientation
|
|
@@ -2768,13 +3651,63 @@ var API = class {
|
|
|
2768
3651
|
this.history = [];
|
|
2769
3652
|
}
|
|
2770
3653
|
}
|
|
3654
|
+
reset() {
|
|
3655
|
+
let graph2 = new Graph({ options: this.options.graph });
|
|
3656
|
+
this.state = { graph: graph2, update: null };
|
|
3657
|
+
this.seq = [this.state];
|
|
3658
|
+
this.index = 0;
|
|
3659
|
+
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3660
|
+
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3661
|
+
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3662
|
+
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3663
|
+
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3664
|
+
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3665
|
+
this.nextNodeId = 1;
|
|
3666
|
+
this.nextEdgeId = 1;
|
|
3667
|
+
}
|
|
3668
|
+
/** Initialize the API */
|
|
2771
3669
|
async init() {
|
|
2772
3670
|
const root = document.getElementById(this.root);
|
|
2773
3671
|
if (!root) throw new Error("root element not found");
|
|
2774
3672
|
root.appendChild(this.canvas.container);
|
|
3673
|
+
await this.applyHistory();
|
|
3674
|
+
}
|
|
3675
|
+
async applyHistory() {
|
|
2775
3676
|
for (const update of this.history)
|
|
2776
3677
|
await this.applyUpdate(update);
|
|
2777
3678
|
}
|
|
3679
|
+
/** Current history index (0-based) */
|
|
3680
|
+
getHistoryIndex() {
|
|
3681
|
+
return this.index;
|
|
3682
|
+
}
|
|
3683
|
+
/** Current history length */
|
|
3684
|
+
getHistoryLength() {
|
|
3685
|
+
return this.seq.length;
|
|
3686
|
+
}
|
|
3687
|
+
/** Toggle canvas editable mode without re-creating the graph */
|
|
3688
|
+
setEditable(editable) {
|
|
3689
|
+
this.canvas.editMode.editable = editable;
|
|
3690
|
+
}
|
|
3691
|
+
/** Replace entire history (clears prior) */
|
|
3692
|
+
async replaceHistory(frames) {
|
|
3693
|
+
this.reset();
|
|
3694
|
+
this.history = frames;
|
|
3695
|
+
await this.applyHistory();
|
|
3696
|
+
}
|
|
3697
|
+
/** Rebuild from snapshot (nodes/edges) */
|
|
3698
|
+
async replaceSnapshot(nodes, edges, description) {
|
|
3699
|
+
this.reset();
|
|
3700
|
+
this.history = [{
|
|
3701
|
+
addNodes: nodes,
|
|
3702
|
+
addEdges: edges,
|
|
3703
|
+
description
|
|
3704
|
+
}];
|
|
3705
|
+
await this.applyHistory();
|
|
3706
|
+
}
|
|
3707
|
+
get graph() {
|
|
3708
|
+
return this.state.graph;
|
|
3709
|
+
}
|
|
3710
|
+
/** Navigate to a different state */
|
|
2778
3711
|
nav(nav) {
|
|
2779
3712
|
let newIndex;
|
|
2780
3713
|
switch (nav) {
|
|
@@ -2796,6 +3729,8 @@ var API = class {
|
|
|
2796
3729
|
this.applyDiff(this.index, newIndex);
|
|
2797
3730
|
this.index = newIndex;
|
|
2798
3731
|
this.state = this.seq[this.index];
|
|
3732
|
+
if (this.events.historyChange)
|
|
3733
|
+
this.events.historyChange(this.index, this.seq.length);
|
|
2799
3734
|
}
|
|
2800
3735
|
applyDiff(oldIndex, newIndex) {
|
|
2801
3736
|
const oldGraph = this.seq[oldIndex].graph;
|
|
@@ -2805,36 +3740,72 @@ var API = class {
|
|
|
2805
3740
|
if (!newNode) this.canvas.deleteNode(oldNode);
|
|
2806
3741
|
}
|
|
2807
3742
|
for (const newNode of newGraph.nodes.values()) {
|
|
2808
|
-
|
|
3743
|
+
const oldNode = oldGraph.nodes.get(newNode.id);
|
|
3744
|
+
if (!oldNode) {
|
|
3745
|
+
this.canvas.addNode(newNode);
|
|
3746
|
+
} else if (oldNode.key !== newNode.key) {
|
|
3747
|
+
this.canvas.deleteNode(oldNode);
|
|
3748
|
+
this.canvas.addNode(newNode);
|
|
3749
|
+
} else if (oldNode.pos !== newNode.pos) {
|
|
3750
|
+
this.canvas.getNode(newNode.key).setPos(newNode.pos);
|
|
3751
|
+
}
|
|
2809
3752
|
}
|
|
2810
3753
|
for (const oldSeg of oldGraph.segs.values()) {
|
|
2811
3754
|
const newSeg = newGraph.segs.get(oldSeg.id);
|
|
2812
|
-
if (!newSeg)
|
|
3755
|
+
if (!newSeg) {
|
|
2813
3756
|
this.canvas.deleteSeg(oldSeg);
|
|
2814
|
-
else if (oldSeg
|
|
2815
|
-
this.canvas.updateSeg(newSeg);
|
|
3757
|
+
} else if (oldSeg !== newSeg) {
|
|
3758
|
+
this.canvas.updateSeg(newSeg, newGraph);
|
|
3759
|
+
}
|
|
2816
3760
|
}
|
|
2817
3761
|
for (const newSeg of newGraph.segs.values()) {
|
|
2818
|
-
if (!oldGraph.segs.has(newSeg.id))
|
|
3762
|
+
if (!oldGraph.segs.has(newSeg.id)) {
|
|
2819
3763
|
this.canvas.addSeg(newSeg, newGraph);
|
|
3764
|
+
}
|
|
2820
3765
|
}
|
|
2821
3766
|
this.canvas.update();
|
|
2822
3767
|
}
|
|
3768
|
+
/** Add a node */
|
|
2823
3769
|
async addNode(node) {
|
|
2824
3770
|
await this.update((update) => update.addNode(node));
|
|
2825
3771
|
}
|
|
3772
|
+
/** Delete a node */
|
|
2826
3773
|
async deleteNode(node) {
|
|
2827
3774
|
await this.update((update) => update.deleteNode(node));
|
|
2828
3775
|
}
|
|
3776
|
+
/** Update a node */
|
|
2829
3777
|
async updateNode(node) {
|
|
2830
3778
|
await this.update((update) => update.updateNode(node));
|
|
2831
3779
|
}
|
|
3780
|
+
/** Add an edge */
|
|
2832
3781
|
async addEdge(edge) {
|
|
2833
3782
|
await this.update((update) => update.addEdge(edge));
|
|
2834
3783
|
}
|
|
3784
|
+
/** Delete an edge */
|
|
2835
3785
|
async deleteEdge(edge) {
|
|
2836
3786
|
await this.update((update) => update.deleteEdge(edge));
|
|
2837
3787
|
}
|
|
3788
|
+
/** Update an edge */
|
|
3789
|
+
async updateEdge(edge) {
|
|
3790
|
+
await this.update((update) => update.updateEdge(edge));
|
|
3791
|
+
}
|
|
3792
|
+
/** Perform a batch of updates */
|
|
3793
|
+
async update(callback) {
|
|
3794
|
+
const updater = new Updater();
|
|
3795
|
+
callback(updater);
|
|
3796
|
+
await this.applyUpdate(updater.update);
|
|
3797
|
+
}
|
|
3798
|
+
/** Rebuild the graph from scratch (removes all then re-adds all nodes/edges) */
|
|
3799
|
+
async rebuild() {
|
|
3800
|
+
const nodes = [...this.nodeIds.keys()];
|
|
3801
|
+
const edges = [...this.edgeIds.keys()];
|
|
3802
|
+
await this.update((updater) => {
|
|
3803
|
+
for (const edge of edges) updater.deleteEdge(edge);
|
|
3804
|
+
for (const node of nodes) updater.deleteNode(node);
|
|
3805
|
+
for (const node of nodes) updater.addNode(node);
|
|
3806
|
+
for (const edge of edges) updater.addEdge(edge);
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
2838
3809
|
async applyUpdate(update) {
|
|
2839
3810
|
log11.info("applyUpdate", update);
|
|
2840
3811
|
const nodes = await this.measureNodes(update);
|
|
@@ -2851,12 +3822,16 @@ var API = class {
|
|
|
2851
3822
|
this._addEdge(edge, mut);
|
|
2852
3823
|
for (const edge of update.updateEdges ?? [])
|
|
2853
3824
|
this._updateEdge(edge, mut);
|
|
3825
|
+
this.nodeOverrides.clear();
|
|
3826
|
+
this.edgeOverrides.clear();
|
|
2854
3827
|
});
|
|
2855
3828
|
this.state = { graph: graph2, update };
|
|
2856
3829
|
this.setNodePositions();
|
|
2857
3830
|
this.seq.splice(this.index + 1);
|
|
2858
3831
|
this.seq.push(this.state);
|
|
2859
3832
|
this.nav("last");
|
|
3833
|
+
if (this.events.historyChange)
|
|
3834
|
+
this.events.historyChange(this.index, this.seq.length);
|
|
2860
3835
|
}
|
|
2861
3836
|
setNodePositions() {
|
|
2862
3837
|
const { graph: graph2 } = this.state;
|
|
@@ -2866,11 +3841,6 @@ var API = class {
|
|
|
2866
3841
|
this.canvas.getNode(node.key).setPos(node.pos);
|
|
2867
3842
|
}
|
|
2868
3843
|
}
|
|
2869
|
-
async update(callback) {
|
|
2870
|
-
const updater = new Updater();
|
|
2871
|
-
callback(updater);
|
|
2872
|
-
await this.applyUpdate(updater.update);
|
|
2873
|
-
}
|
|
2874
3844
|
async measureNodes(update) {
|
|
2875
3845
|
const data = [];
|
|
2876
3846
|
for (const set of [update.updateNodes, update.addNodes])
|
|
@@ -2885,7 +3855,10 @@ var API = class {
|
|
|
2885
3855
|
else if (!data) throw new Error(`invalid node ${data}`);
|
|
2886
3856
|
else if (typeof data == "string") props = { id: data };
|
|
2887
3857
|
else if (typeof data == "object") props = data;
|
|
2888
|
-
else throw new Error(`invalid node ${data}`);
|
|
3858
|
+
else throw new Error(`invalid node ${JSON.stringify(data)}`);
|
|
3859
|
+
this.detectNodeFields(data);
|
|
3860
|
+
const overrides = this.nodeOverrides.get(data);
|
|
3861
|
+
if (overrides) props = { ...props, ...overrides };
|
|
2889
3862
|
let { id, title, text, type, render } = props;
|
|
2890
3863
|
id ??= this.getNodeId(data);
|
|
2891
3864
|
const ports = this.parsePorts(props.ports);
|
|
@@ -2895,6 +3868,21 @@ var API = class {
|
|
|
2895
3868
|
this.nodeVersions.set(data, version);
|
|
2896
3869
|
return { id, data, ports, title, text, type, render, version };
|
|
2897
3870
|
}
|
|
3871
|
+
detectNodeFields(data) {
|
|
3872
|
+
if (typeof data != "object" || !data) return;
|
|
3873
|
+
const skip = /* @__PURE__ */ new Set(["id", "ports", "render", "version"]);
|
|
3874
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3875
|
+
if (skip.has(key)) continue;
|
|
3876
|
+
if (value === null || value === void 0) continue;
|
|
3877
|
+
const type = typeof value;
|
|
3878
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
3879
|
+
this.nodeFields.set(key, type);
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
getNodeFields() {
|
|
3884
|
+
return this.nodeFields;
|
|
3885
|
+
}
|
|
2898
3886
|
parseEdge(data) {
|
|
2899
3887
|
const get = this.options.props.edge;
|
|
2900
3888
|
let props;
|
|
@@ -2903,8 +3891,10 @@ var API = class {
|
|
|
2903
3891
|
else if (typeof data == "string") props = this.parseStringEdge(data);
|
|
2904
3892
|
else if (typeof data == "object") props = data;
|
|
2905
3893
|
else throw new Error(`invalid edge ${data}`);
|
|
3894
|
+
const overrides = this.edgeOverrides.get(data);
|
|
3895
|
+
if (overrides) props = { ...props, ...overrides };
|
|
2906
3896
|
let { id, source, target, type } = props;
|
|
2907
|
-
id
|
|
3897
|
+
if (!id) id = this.getEdgeId(data);
|
|
2908
3898
|
source = this.parseEdgeEnd(source);
|
|
2909
3899
|
target = this.parseEdgeEnd(target);
|
|
2910
3900
|
const edge = { id, source, target, type, data };
|
|
@@ -2917,14 +3907,14 @@ var API = class {
|
|
|
2917
3907
|
const keys = Object.keys(end);
|
|
2918
3908
|
const pidx = keys.indexOf("port");
|
|
2919
3909
|
if (pidx != -1) {
|
|
2920
|
-
if (typeof end.port != "string") return end;
|
|
3910
|
+
if (end.port !== void 0 && typeof end.port != "string") return end;
|
|
2921
3911
|
keys.splice(pidx, 1);
|
|
2922
3912
|
}
|
|
2923
3913
|
if (keys.length != 1) return end;
|
|
2924
3914
|
if (keys[0] == "id") return end;
|
|
2925
3915
|
if (keys[0] != "node") return end;
|
|
2926
3916
|
const id = this.nodeIds.get(end.node);
|
|
2927
|
-
if (!id) throw new Error(`edge end
|
|
3917
|
+
if (!id) throw new Error(`edge end references unknown node ${end.node}`);
|
|
2928
3918
|
return { id, port: end.port };
|
|
2929
3919
|
}
|
|
2930
3920
|
throw new Error(`invalid edge end ${end}`);
|
|
@@ -2934,13 +3924,19 @@ var API = class {
|
|
|
2934
3924
|
return { source, target };
|
|
2935
3925
|
}
|
|
2936
3926
|
parsePorts(ports) {
|
|
2937
|
-
const fixed = {
|
|
3927
|
+
const fixed = {};
|
|
2938
3928
|
for (const key of ["in", "out"]) {
|
|
2939
3929
|
if (ports?.[key] && ports[key].length > 0)
|
|
2940
3930
|
fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
|
|
2941
3931
|
}
|
|
2942
3932
|
return fixed;
|
|
2943
3933
|
}
|
|
3934
|
+
getNode(id) {
|
|
3935
|
+
return this.graph.getNode(id);
|
|
3936
|
+
}
|
|
3937
|
+
getEdge(id) {
|
|
3938
|
+
return this.graph.getEdge(id);
|
|
3939
|
+
}
|
|
2944
3940
|
getNodeId(node) {
|
|
2945
3941
|
let id = this.nodeIds.get(node);
|
|
2946
3942
|
if (!id) {
|
|
@@ -2960,7 +3956,6 @@ var API = class {
|
|
|
2960
3956
|
_addNode(node, mut) {
|
|
2961
3957
|
const { data, id: newId } = node.data;
|
|
2962
3958
|
const oldId = this.nodeIds.get(data);
|
|
2963
|
-
console.log("addNode", node, oldId, newId);
|
|
2964
3959
|
if (oldId && oldId != newId)
|
|
2965
3960
|
throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
|
|
2966
3961
|
this.nodeIds.set(data, newId);
|
|
@@ -2968,36 +3963,1152 @@ var API = class {
|
|
|
2968
3963
|
}
|
|
2969
3964
|
_removeNode(node, mut) {
|
|
2970
3965
|
const id = this.nodeIds.get(node);
|
|
2971
|
-
if (!id) throw new Error(`removing node ${node} which does not exist`);
|
|
3966
|
+
if (!id) throw new Error(`removing node ${JSON.stringify(node)} which does not exist`);
|
|
2972
3967
|
mut.removeNode({ id });
|
|
2973
3968
|
}
|
|
2974
3969
|
_updateNode(node, mut) {
|
|
2975
3970
|
const { data, id: newId } = node.data;
|
|
2976
3971
|
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}`);
|
|
3972
|
+
if (!oldId) throw new Error(`updating unknown node ${JSON.stringify(node)} `);
|
|
3973
|
+
if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId} `);
|
|
2979
3974
|
mut.updateNode(node.data);
|
|
2980
3975
|
}
|
|
2981
3976
|
_addEdge(edge, mut) {
|
|
2982
3977
|
const data = this.parseEdge(edge);
|
|
2983
3978
|
const id = this.edgeIds.get(edge);
|
|
2984
3979
|
if (id && id != data.id)
|
|
2985
|
-
throw new Error(`edge id changed from ${id} to ${data.id}`);
|
|
3980
|
+
throw new Error(`edge id changed from ${id} to ${data.id} `);
|
|
2986
3981
|
this.edgeIds.set(edge, data.id);
|
|
2987
3982
|
mut.addEdge(data);
|
|
2988
3983
|
}
|
|
2989
3984
|
_removeEdge(edge, mut) {
|
|
2990
3985
|
const id = this.edgeIds.get(edge);
|
|
2991
|
-
if (!id) throw new Error(`removing edge ${edge} which does not exist`);
|
|
3986
|
+
if (!id) throw new Error(`removing edge ${JSON.stringify(edge)} which does not exist`);
|
|
2992
3987
|
mut.removeEdge(this.parseEdge(edge));
|
|
2993
3988
|
}
|
|
2994
3989
|
_updateEdge(edge, mut) {
|
|
2995
3990
|
const id = this.edgeIds.get(edge);
|
|
2996
|
-
if (!id) throw new Error(`updating unknown edge ${edge}`);
|
|
3991
|
+
if (!id) throw new Error(`updating unknown edge ${JSON.stringify(edge)} `);
|
|
2997
3992
|
const data = this.parseEdge(edge);
|
|
2998
|
-
if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
|
|
3993
|
+
if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id} `);
|
|
2999
3994
|
mut.updateEdge(data);
|
|
3000
3995
|
}
|
|
3996
|
+
// Event Handlers
|
|
3997
|
+
handleClickNode(id) {
|
|
3998
|
+
const handler = this.events.nodeClick;
|
|
3999
|
+
const node = this.graph.getNode(id);
|
|
4000
|
+
if (handler) handler(node.data);
|
|
4001
|
+
}
|
|
4002
|
+
handleClickEdge(id) {
|
|
4003
|
+
const handler = this.events.edgeClick;
|
|
4004
|
+
if (!handler) return;
|
|
4005
|
+
const seg = this.graph.getSeg(id);
|
|
4006
|
+
if (seg.edgeIds.size != 1) return;
|
|
4007
|
+
const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
|
|
4008
|
+
handler(edge.data);
|
|
4009
|
+
}
|
|
4010
|
+
async handleNewNode() {
|
|
4011
|
+
const gotNode = async (node) => {
|
|
4012
|
+
await this.addNode(node);
|
|
4013
|
+
};
|
|
4014
|
+
if (this.events.newNode)
|
|
4015
|
+
this.events.newNode(gotNode);
|
|
4016
|
+
else
|
|
4017
|
+
this.canvas.showNewNodeModal(async (data) => {
|
|
4018
|
+
if (this.events.addNode)
|
|
4019
|
+
this.events.addNode(data, gotNode);
|
|
4020
|
+
else
|
|
4021
|
+
await gotNode(data);
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
async handleNewNodeFrom(source) {
|
|
4025
|
+
const gotNode = async (node) => {
|
|
4026
|
+
const gotEdge = async (edge) => {
|
|
4027
|
+
await this.update((u) => {
|
|
4028
|
+
u.addNode(node).addEdge(edge);
|
|
4029
|
+
});
|
|
4030
|
+
};
|
|
4031
|
+
const data = this.graph.getNode(source.id).data;
|
|
4032
|
+
const newEdge = {
|
|
4033
|
+
source: { node: data, port: source.port },
|
|
4034
|
+
target: { node }
|
|
4035
|
+
};
|
|
4036
|
+
if (this.events.addEdge)
|
|
4037
|
+
this.events.addEdge(newEdge, gotEdge);
|
|
4038
|
+
else
|
|
4039
|
+
await gotEdge(newEdge);
|
|
4040
|
+
};
|
|
4041
|
+
if (this.events.newNode)
|
|
4042
|
+
this.events.newNode(gotNode);
|
|
4043
|
+
else
|
|
4044
|
+
this.canvas.showNewNodeModal(async (data) => {
|
|
4045
|
+
if (this.events.addNode)
|
|
4046
|
+
this.events.addNode(data, gotNode);
|
|
4047
|
+
else
|
|
4048
|
+
await gotNode(data);
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
async handleEditNode(id) {
|
|
4052
|
+
const node = this.graph.getNode(id);
|
|
4053
|
+
const gotNode = async (node2) => {
|
|
4054
|
+
if (node2) await this.updateNode(node2);
|
|
4055
|
+
};
|
|
4056
|
+
if (this.events.editNode)
|
|
4057
|
+
this.events.editNode(node.data, gotNode);
|
|
4058
|
+
else {
|
|
4059
|
+
this.canvas.showEditNodeModal(node, async (data) => {
|
|
4060
|
+
if (this.events.updateNode)
|
|
4061
|
+
this.events.updateNode(node.data, data, gotNode);
|
|
4062
|
+
else {
|
|
4063
|
+
this.nodeOverrides.set(node.data, data);
|
|
4064
|
+
await gotNode(node.data);
|
|
4065
|
+
}
|
|
4066
|
+
});
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
async handleEditEdge(id) {
|
|
4070
|
+
const seg = this.graph.getSeg(id);
|
|
4071
|
+
if (seg.edgeIds.size != 1) return;
|
|
4072
|
+
const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
|
|
4073
|
+
const gotEdge = async (edge2) => {
|
|
4074
|
+
if (edge2) await this.updateEdge(edge2);
|
|
4075
|
+
};
|
|
4076
|
+
if (this.events.editEdge)
|
|
4077
|
+
this.events.editEdge(edge.data, gotEdge);
|
|
4078
|
+
else
|
|
4079
|
+
this.canvas.showEditEdgeModal(edge, async (data) => {
|
|
4080
|
+
const sourceNode = edge.sourceNode(this.graph);
|
|
4081
|
+
const targetNode = edge.targetNode(this.graph);
|
|
4082
|
+
const update = {
|
|
4083
|
+
source: { node: sourceNode.data, port: data.source.port, marker: data.source.marker },
|
|
4084
|
+
target: { node: targetNode.data, port: data.target.port, marker: data.target.marker }
|
|
4085
|
+
};
|
|
4086
|
+
if (this.events.updateEdge)
|
|
4087
|
+
this.events.updateEdge(edge.data, update, gotEdge);
|
|
4088
|
+
else {
|
|
4089
|
+
this.edgeOverrides.set(edge.data, {
|
|
4090
|
+
source: { id: sourceNode.id, port: data.source.port, marker: data.source.marker },
|
|
4091
|
+
target: { id: targetNode.id, port: data.target.port, marker: data.target.marker },
|
|
4092
|
+
type: data.type
|
|
4093
|
+
});
|
|
4094
|
+
await gotEdge(edge.data);
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
async handleAddEdge(data) {
|
|
4099
|
+
const gotEdge = async (edge) => {
|
|
4100
|
+
if (edge) await this.addEdge(edge);
|
|
4101
|
+
};
|
|
4102
|
+
const newEdge = {
|
|
4103
|
+
source: { node: this.graph.getNode(data.source.id).data, port: data.source.port, marker: data.source.marker },
|
|
4104
|
+
target: { node: this.graph.getNode(data.target.id).data, port: data.target.port, marker: data.target.marker }
|
|
4105
|
+
};
|
|
4106
|
+
if (this.events.addEdge)
|
|
4107
|
+
this.events.addEdge(newEdge, gotEdge);
|
|
4108
|
+
else
|
|
4109
|
+
await gotEdge(data);
|
|
4110
|
+
}
|
|
4111
|
+
async handleDeleteNode(id) {
|
|
4112
|
+
const node = this.getNode(id);
|
|
4113
|
+
if (this.events.removeNode)
|
|
4114
|
+
this.events.removeNode(node.data, async (remove) => {
|
|
4115
|
+
if (remove) await this.deleteNode(node.data);
|
|
4116
|
+
});
|
|
4117
|
+
else
|
|
4118
|
+
await this.deleteNode(node.data);
|
|
4119
|
+
}
|
|
4120
|
+
async handleDeleteEdge(id) {
|
|
4121
|
+
const edge = this.getEdge(id);
|
|
4122
|
+
if (this.events.removeEdge)
|
|
4123
|
+
this.events.removeEdge(edge.data, async (remove) => {
|
|
4124
|
+
if (remove) await this.deleteEdge(edge.data);
|
|
4125
|
+
});
|
|
4126
|
+
else
|
|
4127
|
+
await this.deleteEdge(edge.data);
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
|
|
4131
|
+
// src/api/ingest.ts
|
|
4132
|
+
var Ingest = class {
|
|
4133
|
+
constructor(api) {
|
|
4134
|
+
this.api = api;
|
|
4135
|
+
}
|
|
4136
|
+
/**
|
|
4137
|
+
* Apply an incoming ingest message to the API.
|
|
4138
|
+
* - snapshot: rebuild state from nodes/edges (clears prior history)
|
|
4139
|
+
* - update: apply incremental update
|
|
4140
|
+
* - history: initialize from a set of frames (clears prior history)
|
|
4141
|
+
*/
|
|
4142
|
+
async apply(msg) {
|
|
4143
|
+
switch (msg.type) {
|
|
4144
|
+
case "snapshot": {
|
|
4145
|
+
await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
|
|
4146
|
+
break;
|
|
4147
|
+
}
|
|
4148
|
+
case "update": {
|
|
4149
|
+
await this.api.update((u) => {
|
|
4150
|
+
if (msg.addNodes) u.addNodes(...msg.addNodes);
|
|
4151
|
+
if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
|
|
4152
|
+
if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
|
|
4153
|
+
if (msg.addEdges) u.addEdges(...msg.addEdges);
|
|
4154
|
+
if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
|
|
4155
|
+
if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
|
|
4156
|
+
if (msg.description) u.describe(msg.description);
|
|
4157
|
+
});
|
|
4158
|
+
break;
|
|
4159
|
+
}
|
|
4160
|
+
case "history": {
|
|
4161
|
+
await this.api.replaceHistory(msg.frames);
|
|
4162
|
+
break;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
};
|
|
4167
|
+
|
|
4168
|
+
// src/api/sources/WebSocketSource.ts
|
|
4169
|
+
var WebSocketSource = class {
|
|
4170
|
+
url;
|
|
4171
|
+
ws = null;
|
|
4172
|
+
onMessage;
|
|
4173
|
+
onStatus;
|
|
4174
|
+
reconnectMs;
|
|
4175
|
+
closedByUser = false;
|
|
4176
|
+
connectStartTime = null;
|
|
4177
|
+
totalTimeoutMs = 1e4;
|
|
4178
|
+
totalTimeoutTimer = null;
|
|
4179
|
+
constructor(url, onMessage, onStatus, reconnectMs = 1500) {
|
|
4180
|
+
this.url = url;
|
|
4181
|
+
this.onMessage = onMessage;
|
|
4182
|
+
this.onStatus = onStatus;
|
|
4183
|
+
this.reconnectMs = reconnectMs;
|
|
4184
|
+
}
|
|
4185
|
+
connect() {
|
|
4186
|
+
this.closedByUser = false;
|
|
4187
|
+
this.connectStartTime = Date.now();
|
|
4188
|
+
this.startTotalTimeout();
|
|
4189
|
+
this.open();
|
|
4190
|
+
}
|
|
4191
|
+
disconnect() {
|
|
4192
|
+
this.closedByUser = true;
|
|
4193
|
+
this.clearTotalTimeout();
|
|
4194
|
+
if (this.ws) {
|
|
4195
|
+
try {
|
|
4196
|
+
this.ws.close();
|
|
4197
|
+
} catch {
|
|
4198
|
+
}
|
|
4199
|
+
this.ws = null;
|
|
4200
|
+
}
|
|
4201
|
+
this.onStatus?.("closed");
|
|
4202
|
+
}
|
|
4203
|
+
startTotalTimeout() {
|
|
4204
|
+
this.clearTotalTimeout();
|
|
4205
|
+
this.totalTimeoutTimer = window.setTimeout(() => {
|
|
4206
|
+
if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
|
|
4207
|
+
this.closedByUser = true;
|
|
4208
|
+
if (this.ws) {
|
|
4209
|
+
try {
|
|
4210
|
+
this.ws.close();
|
|
4211
|
+
} catch {
|
|
4212
|
+
}
|
|
4213
|
+
this.ws = null;
|
|
4214
|
+
}
|
|
4215
|
+
this.clearTotalTimeout();
|
|
4216
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4217
|
+
}
|
|
4218
|
+
}, this.totalTimeoutMs);
|
|
4219
|
+
}
|
|
4220
|
+
clearTotalTimeout() {
|
|
4221
|
+
if (this.totalTimeoutTimer !== null) {
|
|
4222
|
+
clearTimeout(this.totalTimeoutTimer);
|
|
4223
|
+
this.totalTimeoutTimer = null;
|
|
4224
|
+
}
|
|
4225
|
+
this.connectStartTime = null;
|
|
4226
|
+
}
|
|
4227
|
+
open() {
|
|
4228
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4229
|
+
if (!this.closedByUser) {
|
|
4230
|
+
this.closedByUser = true;
|
|
4231
|
+
this.clearTotalTimeout();
|
|
4232
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4233
|
+
}
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
this.onStatus?.(this.ws ? "reconnecting" : "connecting");
|
|
4237
|
+
const ws = new WebSocket(this.url);
|
|
4238
|
+
this.ws = ws;
|
|
4239
|
+
ws.onopen = () => {
|
|
4240
|
+
this.clearTotalTimeout();
|
|
4241
|
+
this.onStatus?.("connected");
|
|
4242
|
+
};
|
|
4243
|
+
ws.onerror = (e) => {
|
|
4244
|
+
this.onStatus?.("error", e);
|
|
4245
|
+
};
|
|
4246
|
+
ws.onclose = () => {
|
|
4247
|
+
if (this.closedByUser) {
|
|
4248
|
+
this.onStatus?.("closed");
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4252
|
+
this.closedByUser = true;
|
|
4253
|
+
this.clearTotalTimeout();
|
|
4254
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
this.onStatus?.("reconnecting");
|
|
4258
|
+
setTimeout(() => this.open(), this.reconnectMs);
|
|
4259
|
+
};
|
|
4260
|
+
ws.onmessage = (ev) => {
|
|
4261
|
+
const data = typeof ev.data === "string" ? ev.data : "";
|
|
4262
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4263
|
+
for (const line of lines) {
|
|
4264
|
+
try {
|
|
4265
|
+
const obj = JSON.parse(line);
|
|
4266
|
+
this.onMessage(obj);
|
|
4267
|
+
} catch {
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
|
|
4274
|
+
// src/api/sources/FileSystemSource.ts
|
|
4275
|
+
var FileSystemSource = class {
|
|
4276
|
+
handle = null;
|
|
4277
|
+
onMessage;
|
|
4278
|
+
onStatus;
|
|
4279
|
+
timer = null;
|
|
4280
|
+
lastSize = 0;
|
|
4281
|
+
filename;
|
|
4282
|
+
intervalMs;
|
|
4283
|
+
constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
|
|
4284
|
+
this.onMessage = onMessage;
|
|
4285
|
+
this.onStatus = onStatus;
|
|
4286
|
+
this.filename = filename;
|
|
4287
|
+
this.intervalMs = intervalMs;
|
|
4288
|
+
}
|
|
4289
|
+
async openDirectory() {
|
|
4290
|
+
try {
|
|
4291
|
+
const dir = await window.showDirectoryPicker?.();
|
|
4292
|
+
if (!dir) throw new Error("File System Access not supported or cancelled");
|
|
4293
|
+
const handle = await dir.getFileHandle(this.filename, { create: false });
|
|
4294
|
+
this.handle = handle;
|
|
4295
|
+
this.onStatus?.("opened", { file: this.filename });
|
|
4296
|
+
this.lastSize = 0;
|
|
4297
|
+
this.startPolling();
|
|
4298
|
+
} catch (e) {
|
|
4299
|
+
this.onStatus?.("error", e);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
close() {
|
|
4303
|
+
if (this.timer) {
|
|
4304
|
+
window.clearInterval(this.timer);
|
|
4305
|
+
this.timer = null;
|
|
4306
|
+
}
|
|
4307
|
+
this.handle = null;
|
|
4308
|
+
this.onStatus?.("closed");
|
|
4309
|
+
}
|
|
4310
|
+
startPolling() {
|
|
4311
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4312
|
+
this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
|
|
4313
|
+
}
|
|
4314
|
+
async readNewLines() {
|
|
4315
|
+
try {
|
|
4316
|
+
if (!this.handle) return;
|
|
4317
|
+
this.onStatus?.("reading");
|
|
4318
|
+
const file = await this.handle.getFile();
|
|
4319
|
+
if (file.size === this.lastSize) return;
|
|
4320
|
+
const slice = await file.slice(this.lastSize).text();
|
|
4321
|
+
this.lastSize = file.size;
|
|
4322
|
+
const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4323
|
+
for (const line of lines) {
|
|
4324
|
+
try {
|
|
4325
|
+
const obj = JSON.parse(line);
|
|
4326
|
+
this.onMessage(obj);
|
|
4327
|
+
} catch {
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
} catch (e) {
|
|
4331
|
+
this.onStatus?.("error", e);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
|
|
4336
|
+
// src/api/sources/FileSource.ts
|
|
4337
|
+
var FileSource = class {
|
|
4338
|
+
url;
|
|
4339
|
+
onMessage;
|
|
4340
|
+
onStatus;
|
|
4341
|
+
timer = null;
|
|
4342
|
+
lastETag = null;
|
|
4343
|
+
lastContent = "";
|
|
4344
|
+
intervalMs = 1e3;
|
|
4345
|
+
closed = false;
|
|
4346
|
+
constructor(url, onMessage, onStatus, intervalMs = 1e3) {
|
|
4347
|
+
this.url = url;
|
|
4348
|
+
this.onMessage = onMessage;
|
|
4349
|
+
this.onStatus = onStatus;
|
|
4350
|
+
this.intervalMs = intervalMs;
|
|
4351
|
+
}
|
|
4352
|
+
async connect() {
|
|
4353
|
+
this.closed = false;
|
|
4354
|
+
this.lastETag = null;
|
|
4355
|
+
this.lastContent = "";
|
|
4356
|
+
this.onStatus?.("opened");
|
|
4357
|
+
this.startPolling();
|
|
4358
|
+
}
|
|
4359
|
+
close() {
|
|
4360
|
+
this.closed = true;
|
|
4361
|
+
if (this.timer) {
|
|
4362
|
+
window.clearInterval(this.timer);
|
|
4363
|
+
this.timer = null;
|
|
4364
|
+
}
|
|
4365
|
+
this.onStatus?.("closed");
|
|
4366
|
+
}
|
|
4367
|
+
startPolling() {
|
|
4368
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4369
|
+
this.timer = window.setInterval(() => this.poll(), this.intervalMs);
|
|
4370
|
+
this.poll();
|
|
4371
|
+
}
|
|
4372
|
+
async poll() {
|
|
4373
|
+
if (this.closed) return;
|
|
4374
|
+
try {
|
|
4375
|
+
this.onStatus?.("reading");
|
|
4376
|
+
const headers = {};
|
|
4377
|
+
if (this.lastETag) {
|
|
4378
|
+
headers["If-None-Match"] = this.lastETag;
|
|
4379
|
+
}
|
|
4380
|
+
const response = await fetch(this.url, { headers });
|
|
4381
|
+
if (response.status === 304) {
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
if (!response.ok) {
|
|
4385
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4386
|
+
}
|
|
4387
|
+
const etag = response.headers.get("ETag");
|
|
4388
|
+
if (etag) {
|
|
4389
|
+
this.lastETag = etag;
|
|
4390
|
+
}
|
|
4391
|
+
const content = await response.text();
|
|
4392
|
+
if (content === this.lastContent) {
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4396
|
+
const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4397
|
+
const newLines = lines.slice(lastContentLines.length);
|
|
4398
|
+
for (const line of newLines) {
|
|
4399
|
+
try {
|
|
4400
|
+
const obj = JSON.parse(line);
|
|
4401
|
+
this.onMessage(obj);
|
|
4402
|
+
} catch {
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
this.lastContent = content;
|
|
4406
|
+
} catch (e) {
|
|
4407
|
+
this.onStatus?.("error", e);
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
};
|
|
4411
|
+
|
|
4412
|
+
// src/playground/playground.ts
|
|
4413
|
+
var import_styles2 = __toESM(require("./styles.css?raw"), 1);
|
|
4414
|
+
var Playground = class {
|
|
4415
|
+
options;
|
|
4416
|
+
rootElement;
|
|
4417
|
+
currentExample;
|
|
4418
|
+
currentGraph = null;
|
|
4419
|
+
ingest = null;
|
|
4420
|
+
isEditable = false;
|
|
4421
|
+
wsSource = null;
|
|
4422
|
+
fsSource = null;
|
|
4423
|
+
fileSource = null;
|
|
4424
|
+
wsStatus = "disconnected";
|
|
4425
|
+
fsStatus = "disconnected";
|
|
4426
|
+
fileStatus = "disconnected";
|
|
4427
|
+
activeSourceType = null;
|
|
4428
|
+
wsUrl = "ws://localhost:8787";
|
|
4429
|
+
sourceModal = null;
|
|
4430
|
+
helpOverlay = null;
|
|
4431
|
+
exampleList;
|
|
4432
|
+
graphContainerId;
|
|
4433
|
+
constructor(options) {
|
|
4434
|
+
this.options = options;
|
|
4435
|
+
this.exampleList = Object.keys(options.examples);
|
|
4436
|
+
this.currentExample = options.defaultExample || this.exampleList[0];
|
|
4437
|
+
this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
|
|
4438
|
+
if (typeof options.root === "string") {
|
|
4439
|
+
const el = document.getElementById(options.root);
|
|
4440
|
+
if (!el) throw new Error(`Element with id "${options.root}" not found`);
|
|
4441
|
+
this.rootElement = el;
|
|
4442
|
+
} else {
|
|
4443
|
+
this.rootElement = options.root;
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
async init() {
|
|
4447
|
+
this.injectStyles();
|
|
4448
|
+
this.createDOM();
|
|
4449
|
+
this.setupEventListeners();
|
|
4450
|
+
await this.renderGraph();
|
|
4451
|
+
this.updateSourceIcon();
|
|
4452
|
+
this.connectExampleSource();
|
|
4453
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4454
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4455
|
+
}
|
|
4456
|
+
injectStyles() {
|
|
4457
|
+
if (!document.getElementById("g3p-playground-styles")) {
|
|
4458
|
+
const styleEl = document.createElement("style");
|
|
4459
|
+
styleEl.id = "g3p-playground-styles";
|
|
4460
|
+
styleEl.textContent = import_styles2.default;
|
|
4461
|
+
document.head.appendChild(styleEl);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
createDOM() {
|
|
4465
|
+
const exampleList = this.exampleList.map((key, i) => {
|
|
4466
|
+
const example = this.options.examples[key];
|
|
4467
|
+
const isActive = i === 0 || key === this.currentExample;
|
|
4468
|
+
return `
|
|
4469
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
4470
|
+
${example.name}
|
|
4471
|
+
</button>
|
|
4472
|
+
`;
|
|
4473
|
+
}).join("");
|
|
4474
|
+
this.rootElement.innerHTML = `
|
|
4475
|
+
<main class="playground">
|
|
4476
|
+
<div class="sidebar">
|
|
4477
|
+
<h2>Examples</h2>
|
|
4478
|
+
<div class="example-list">
|
|
4479
|
+
${exampleList}
|
|
4480
|
+
</div>
|
|
4481
|
+
|
|
4482
|
+
<h2>Options</h2>
|
|
4483
|
+
<div class="options">
|
|
4484
|
+
<div class="option-group">
|
|
4485
|
+
<label>Orientation</label>
|
|
4486
|
+
<select id="orientation">
|
|
4487
|
+
<option value="TB">Top to Bottom</option>
|
|
4488
|
+
<option value="BT">Bottom to Top</option>
|
|
4489
|
+
<option value="LR">Left to Right</option>
|
|
4490
|
+
<option value="RL">Right to Left</option>
|
|
4491
|
+
</select>
|
|
4492
|
+
</div>
|
|
4493
|
+
|
|
4494
|
+
<div class="option-group">
|
|
4495
|
+
<label>Port Style</label>
|
|
4496
|
+
<select id="portStyle">
|
|
4497
|
+
<option value="outside">Outside</option>
|
|
4498
|
+
<option value="inside">Inside</option>
|
|
4499
|
+
</select>
|
|
4500
|
+
</div>
|
|
4501
|
+
|
|
4502
|
+
<div class="option-group">
|
|
4503
|
+
<label>
|
|
4504
|
+
<input type="checkbox" id="portLabelRotate" />
|
|
4505
|
+
Rotate Port Labels
|
|
4506
|
+
</label>
|
|
4507
|
+
</div>
|
|
4508
|
+
|
|
4509
|
+
<div class="option-group">
|
|
4510
|
+
<label>Theme</label>
|
|
4511
|
+
<select id="colorMode">
|
|
4512
|
+
<option value="system">System</option>
|
|
4513
|
+
<option value="light">Light</option>
|
|
4514
|
+
<option value="dark">Dark</option>
|
|
4515
|
+
</select>
|
|
4516
|
+
</div>
|
|
4517
|
+
</div>
|
|
4518
|
+
</div>
|
|
4519
|
+
|
|
4520
|
+
<div class="graph-area">
|
|
4521
|
+
<div class="graph-toolbar">
|
|
4522
|
+
<div class="nav-controls">
|
|
4523
|
+
<button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
|
|
4524
|
+
<button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
|
|
4525
|
+
<span id="history-label" style="min-width: 4rem; text-align: center; display: inline-flex; align-items: center; justify-content: center; height: 2.25rem;">\u2014 / \u2014</span>
|
|
4526
|
+
<button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
|
|
4527
|
+
<button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
|
|
4528
|
+
</div>
|
|
4529
|
+
<div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
|
|
4530
|
+
<button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
|
|
4531
|
+
</div>
|
|
4532
|
+
<button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
|
|
4533
|
+
<button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
|
|
4534
|
+
<button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
|
|
4535
|
+
</div>
|
|
4536
|
+
<div class="graph-container" id="${this.graphContainerId}"></div>
|
|
4537
|
+
</div>
|
|
4538
|
+
</main>
|
|
4539
|
+
`;
|
|
4540
|
+
}
|
|
4541
|
+
setupEventListeners() {
|
|
4542
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
|
|
4543
|
+
btn.addEventListener("click", () => {
|
|
4544
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
4545
|
+
btn.classList.add("active");
|
|
4546
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
4547
|
+
this.renderGraph();
|
|
4548
|
+
this.connectExampleSource();
|
|
4549
|
+
});
|
|
4550
|
+
});
|
|
4551
|
+
this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
|
|
4552
|
+
el.addEventListener("change", () => this.renderGraph());
|
|
4553
|
+
});
|
|
4554
|
+
this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
|
|
4555
|
+
this.currentGraph?.nav("first");
|
|
4556
|
+
this.updateHistoryLabel();
|
|
4557
|
+
});
|
|
4558
|
+
this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
|
|
4559
|
+
this.currentGraph?.nav("prev");
|
|
4560
|
+
this.updateHistoryLabel();
|
|
4561
|
+
});
|
|
4562
|
+
this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
|
|
4563
|
+
this.currentGraph?.nav("next");
|
|
4564
|
+
this.updateHistoryLabel();
|
|
4565
|
+
});
|
|
4566
|
+
this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
|
|
4567
|
+
this.currentGraph?.nav("last");
|
|
4568
|
+
this.updateHistoryLabel();
|
|
4569
|
+
});
|
|
4570
|
+
this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
|
|
4571
|
+
this.currentGraph?.rebuild();
|
|
4572
|
+
});
|
|
4573
|
+
this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
|
|
4574
|
+
this.isEditable = !this.isEditable;
|
|
4575
|
+
const btn = this.rootElement.querySelector("#edit-toggle");
|
|
4576
|
+
if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
|
|
4577
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4578
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4579
|
+
try {
|
|
4580
|
+
this.currentGraph?.setEditable?.(this.isEditable);
|
|
4581
|
+
} catch {
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
|
|
4585
|
+
const sourceIconBtn = this.rootElement.querySelector("#source-icon");
|
|
4586
|
+
if (sourceIconBtn) {
|
|
4587
|
+
sourceIconBtn.addEventListener("click", (e) => {
|
|
4588
|
+
e.preventDefault();
|
|
4589
|
+
e.stopPropagation();
|
|
4590
|
+
this.openSourceModal();
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
document.addEventListener("keydown", (e) => {
|
|
4594
|
+
if (!this.currentGraph) return;
|
|
4595
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
|
|
4596
|
+
switch (e.key) {
|
|
4597
|
+
case "Home":
|
|
4598
|
+
this.currentGraph.nav("first");
|
|
4599
|
+
this.updateHistoryLabel();
|
|
4600
|
+
break;
|
|
4601
|
+
case "End":
|
|
4602
|
+
this.currentGraph.nav("last");
|
|
4603
|
+
this.updateHistoryLabel();
|
|
4604
|
+
break;
|
|
4605
|
+
case "ArrowLeft":
|
|
4606
|
+
this.currentGraph.nav("prev");
|
|
4607
|
+
this.updateHistoryLabel();
|
|
4608
|
+
break;
|
|
4609
|
+
case "ArrowRight":
|
|
4610
|
+
this.currentGraph.nav("next");
|
|
4611
|
+
this.updateHistoryLabel();
|
|
4612
|
+
break;
|
|
4613
|
+
}
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
getResolvedColorMode() {
|
|
4617
|
+
const mode = this.rootElement.querySelector("#colorMode")?.value;
|
|
4618
|
+
if (mode === "system") {
|
|
4619
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4620
|
+
}
|
|
4621
|
+
return mode;
|
|
4622
|
+
}
|
|
4623
|
+
getOptions(exampleOptions) {
|
|
4624
|
+
const orientation = this.rootElement.querySelector("#orientation")?.value;
|
|
4625
|
+
return {
|
|
4626
|
+
graph: { orientation },
|
|
4627
|
+
canvas: {
|
|
4628
|
+
width: "100%",
|
|
4629
|
+
height: "100%",
|
|
4630
|
+
colorMode: this.getResolvedColorMode(),
|
|
4631
|
+
editable: this.isEditable,
|
|
4632
|
+
...exampleOptions?.canvas
|
|
4633
|
+
}
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
async renderGraph() {
|
|
4637
|
+
const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
|
|
4638
|
+
if (!container) return;
|
|
4639
|
+
container.innerHTML = "";
|
|
4640
|
+
const example = this.options.examples[this.currentExample];
|
|
4641
|
+
const options = this.getOptions(example.options);
|
|
4642
|
+
try {
|
|
4643
|
+
this.currentGraph = await graph({
|
|
4644
|
+
root: this.graphContainerId,
|
|
4645
|
+
nodes: example.nodes,
|
|
4646
|
+
edges: example.edges,
|
|
4647
|
+
options,
|
|
4648
|
+
events: {
|
|
4649
|
+
historyChange: () => this.updateHistoryLabel()
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
this.ingest = new Ingest(this.currentGraph);
|
|
4653
|
+
this.updateHistoryLabel();
|
|
4654
|
+
} catch (e) {
|
|
4655
|
+
console.error("Failed to render graph:", e);
|
|
4656
|
+
container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
updateHistoryLabel() {
|
|
4660
|
+
const label = this.rootElement.querySelector("#history-label");
|
|
4661
|
+
if (!label || !this.currentGraph) return;
|
|
4662
|
+
try {
|
|
4663
|
+
const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
|
|
4664
|
+
const len = this.currentGraph.getHistoryLength?.() ?? 1;
|
|
4665
|
+
label.textContent = `${idx + 1} / ${len}`;
|
|
4666
|
+
} catch {
|
|
4667
|
+
label.textContent = "\u2014 / \u2014";
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
connectExampleSource() {
|
|
4671
|
+
const example = this.options.examples[this.currentExample];
|
|
4672
|
+
if (!example.source) {
|
|
4673
|
+
this.disconnectAllSources();
|
|
4674
|
+
return;
|
|
4675
|
+
}
|
|
4676
|
+
this.disconnectAllSources();
|
|
4677
|
+
if (example.source.type === "websocket") {
|
|
4678
|
+
this.wsUrl = example.source.url;
|
|
4679
|
+
this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
4680
|
+
this.wsSource.connect();
|
|
4681
|
+
} else if (example.source.type === "file") {
|
|
4682
|
+
this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
|
|
4683
|
+
this.fileSource.connect();
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
disconnectAllSources() {
|
|
4687
|
+
this.wsSource?.disconnect();
|
|
4688
|
+
this.fsSource?.close();
|
|
4689
|
+
this.fileSource?.close();
|
|
4690
|
+
this.wsSource = null;
|
|
4691
|
+
this.fsSource = null;
|
|
4692
|
+
this.fileSource = null;
|
|
4693
|
+
this.activeSourceType = null;
|
|
4694
|
+
this.wsStatus = "disconnected";
|
|
4695
|
+
this.fsStatus = "disconnected";
|
|
4696
|
+
this.fileStatus = "disconnected";
|
|
4697
|
+
this.updateSourceIcon();
|
|
4698
|
+
}
|
|
4699
|
+
openHelp() {
|
|
4700
|
+
if (!this.helpOverlay) {
|
|
4701
|
+
this.helpOverlay = document.createElement("div");
|
|
4702
|
+
this.helpOverlay.className = "modal-overlay";
|
|
4703
|
+
this.helpOverlay.innerHTML = `
|
|
4704
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
4705
|
+
<div class="modal-header">
|
|
4706
|
+
<h3 id="help-title">Editing the Graph</h3>
|
|
4707
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4708
|
+
</div>
|
|
4709
|
+
<div class="modal-body">
|
|
4710
|
+
<p>Here's how to edit the graph:</p>
|
|
4711
|
+
<ul>
|
|
4712
|
+
<li><strong>Enable editing</strong>: Click "Edit"</li>
|
|
4713
|
+
<li><strong>Add a node</strong>: Double\u2011click an empty area</li>
|
|
4714
|
+
<li><strong>Edit a node</strong>: Double\u2011click a node</li>
|
|
4715
|
+
<li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
|
|
4716
|
+
<li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
|
|
4717
|
+
<li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
|
|
4718
|
+
<li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
|
|
4719
|
+
</ul>
|
|
4720
|
+
<p>When you're done, click "Done" to lock the canvas.</p>
|
|
4721
|
+
</div>
|
|
4722
|
+
</div>
|
|
4723
|
+
`;
|
|
4724
|
+
document.body.appendChild(this.helpOverlay);
|
|
4725
|
+
this.helpOverlay.addEventListener("click", (e) => {
|
|
4726
|
+
const target = e.target;
|
|
4727
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4728
|
+
this.closeHelp();
|
|
4729
|
+
}
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4732
|
+
this.helpOverlay.style.display = "flex";
|
|
4733
|
+
}
|
|
4734
|
+
closeHelp() {
|
|
4735
|
+
if (this.helpOverlay) this.helpOverlay.style.display = "none";
|
|
4736
|
+
}
|
|
4737
|
+
handleIngestMessage = async (msg) => {
|
|
4738
|
+
if (!this.ingest) return;
|
|
4739
|
+
await this.ingest.apply(msg);
|
|
4740
|
+
};
|
|
4741
|
+
updateSourceIcon() {
|
|
4742
|
+
const iconBtn = this.rootElement.querySelector("#source-icon");
|
|
4743
|
+
if (!iconBtn) return;
|
|
4744
|
+
iconBtn.classList.remove("active", "connecting", "error");
|
|
4745
|
+
const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
|
|
4746
|
+
const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
|
|
4747
|
+
const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
|
|
4748
|
+
let icon = "\u{1F4E1}";
|
|
4749
|
+
if (this.activeSourceType === "folder") {
|
|
4750
|
+
icon = "\u{1F4C1}";
|
|
4751
|
+
} else if (this.activeSourceType === "file") {
|
|
4752
|
+
icon = "\u{1F4C4}";
|
|
4753
|
+
}
|
|
4754
|
+
if (isConnected) {
|
|
4755
|
+
iconBtn.classList.add("active");
|
|
4756
|
+
iconBtn.textContent = icon;
|
|
4757
|
+
} else if (isConnecting) {
|
|
4758
|
+
iconBtn.classList.add("connecting");
|
|
4759
|
+
iconBtn.textContent = icon;
|
|
4760
|
+
} else if (hasError) {
|
|
4761
|
+
iconBtn.classList.add("error");
|
|
4762
|
+
iconBtn.textContent = icon;
|
|
4763
|
+
} else {
|
|
4764
|
+
iconBtn.textContent = "\u{1F4E1}";
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
updateWsStatus = (status, detail) => {
|
|
4768
|
+
if (status === "connecting" || status === "reconnecting") {
|
|
4769
|
+
this.wsStatus = "connecting";
|
|
4770
|
+
this.activeSourceType = "ws";
|
|
4771
|
+
} else if (status === "connected") {
|
|
4772
|
+
this.wsStatus = "connected";
|
|
4773
|
+
this.activeSourceType = "ws";
|
|
4774
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4775
|
+
this.fsSource.close();
|
|
4776
|
+
}
|
|
4777
|
+
if (this.fileSource && this.fileStatus === "connected") {
|
|
4778
|
+
this.fileSource.close();
|
|
4779
|
+
}
|
|
4780
|
+
} else if (status === "error") {
|
|
4781
|
+
this.wsStatus = "error";
|
|
4782
|
+
} else {
|
|
4783
|
+
this.wsStatus = "disconnected";
|
|
4784
|
+
if (this.activeSourceType === "ws") {
|
|
4785
|
+
this.activeSourceType = null;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
this.updateSourceIcon();
|
|
4789
|
+
this.updateSourceModal();
|
|
4790
|
+
};
|
|
4791
|
+
updateFsStatus = (status, detail) => {
|
|
4792
|
+
if (status === "opened") {
|
|
4793
|
+
this.fsStatus = "opening";
|
|
4794
|
+
this.activeSourceType = "folder";
|
|
4795
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4796
|
+
this.wsSource.disconnect();
|
|
4797
|
+
}
|
|
4798
|
+
} else if (status === "reading") {
|
|
4799
|
+
this.fsStatus = "connected";
|
|
4800
|
+
this.activeSourceType = "folder";
|
|
4801
|
+
} else if (status === "error") {
|
|
4802
|
+
this.fsStatus = "error";
|
|
4803
|
+
} else if (status === "closed") {
|
|
4804
|
+
this.fsStatus = "disconnected";
|
|
4805
|
+
if (this.activeSourceType === "folder") {
|
|
4806
|
+
this.activeSourceType = null;
|
|
4807
|
+
}
|
|
4808
|
+
} else {
|
|
4809
|
+
this.fsStatus = "disconnected";
|
|
4810
|
+
}
|
|
4811
|
+
this.updateSourceIcon();
|
|
4812
|
+
this.updateSourceModal();
|
|
4813
|
+
};
|
|
4814
|
+
updateFileStatus = (status, detail) => {
|
|
4815
|
+
if (status === "opened") {
|
|
4816
|
+
this.fileStatus = "connecting";
|
|
4817
|
+
this.activeSourceType = "file";
|
|
4818
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4819
|
+
this.wsSource.disconnect();
|
|
4820
|
+
}
|
|
4821
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4822
|
+
this.fsSource.close();
|
|
4823
|
+
}
|
|
4824
|
+
} else if (status === "reading") {
|
|
4825
|
+
this.fileStatus = "connected";
|
|
4826
|
+
this.activeSourceType = "file";
|
|
4827
|
+
} else if (status === "error") {
|
|
4828
|
+
this.fileStatus = "error";
|
|
4829
|
+
} else if (status === "closed") {
|
|
4830
|
+
this.fileStatus = "disconnected";
|
|
4831
|
+
if (this.activeSourceType === "file") {
|
|
4832
|
+
this.activeSourceType = null;
|
|
4833
|
+
}
|
|
4834
|
+
} else {
|
|
4835
|
+
this.fileStatus = "disconnected";
|
|
4836
|
+
}
|
|
4837
|
+
this.updateSourceIcon();
|
|
4838
|
+
this.updateSourceModal();
|
|
4839
|
+
};
|
|
4840
|
+
createSourceModal() {
|
|
4841
|
+
if (this.sourceModal) return this.sourceModal;
|
|
4842
|
+
this.sourceModal = document.createElement("div");
|
|
4843
|
+
this.sourceModal.className = "modal-overlay";
|
|
4844
|
+
this.sourceModal.style.display = "none";
|
|
4845
|
+
this.sourceModal.innerHTML = `
|
|
4846
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
|
|
4847
|
+
<div class="modal-header">
|
|
4848
|
+
<h3 id="source-modal-title">Data Source Connection</h3>
|
|
4849
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4850
|
+
</div>
|
|
4851
|
+
<div class="modal-body">
|
|
4852
|
+
<div class="source-type-selector">
|
|
4853
|
+
<button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
|
|
4854
|
+
<button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
|
|
4855
|
+
</div>
|
|
4856
|
+
|
|
4857
|
+
<div class="source-controls" data-source="ws">
|
|
4858
|
+
<div class="form-group">
|
|
4859
|
+
<label for="source-modal-url">WebSocket URL</label>
|
|
4860
|
+
<input type="text" id="source-modal-url" value="${this.wsUrl}" />
|
|
4861
|
+
</div>
|
|
4862
|
+
<div class="button-group">
|
|
4863
|
+
<button id="source-modal-connect-ws" class="primary">Connect</button>
|
|
4864
|
+
<button id="source-modal-disconnect-ws">Disconnect</button>
|
|
4865
|
+
<button id="source-modal-change-ws">Change Connection</button>
|
|
4866
|
+
</div>
|
|
4867
|
+
</div>
|
|
4868
|
+
|
|
4869
|
+
<div class="source-controls" data-source="folder">
|
|
4870
|
+
<div class="form-group">
|
|
4871
|
+
<label>File System Source</label>
|
|
4872
|
+
<p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
|
|
4873
|
+
Select a directory containing a graph.ndjson file to watch for changes.
|
|
4874
|
+
</p>
|
|
4875
|
+
</div>
|
|
4876
|
+
<div class="button-group">
|
|
4877
|
+
<button id="source-modal-connect-folder" class="primary">Open Folder</button>
|
|
4878
|
+
<button id="source-modal-disconnect-folder">Disconnect</button>
|
|
4879
|
+
</div>
|
|
4880
|
+
</div>
|
|
4881
|
+
|
|
4882
|
+
<div id="source-modal-status"></div>
|
|
4883
|
+
</div>
|
|
4884
|
+
</div>
|
|
4885
|
+
`;
|
|
4886
|
+
document.body.appendChild(this.sourceModal);
|
|
4887
|
+
this.sourceModal.addEventListener("click", (e) => {
|
|
4888
|
+
const target = e.target;
|
|
4889
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4890
|
+
this.closeSourceModal();
|
|
4891
|
+
}
|
|
4892
|
+
});
|
|
4893
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4894
|
+
btn.addEventListener("click", () => {
|
|
4895
|
+
const sourceType = btn.getAttribute("data-source");
|
|
4896
|
+
if (sourceType) {
|
|
4897
|
+
this.selectSourceType(sourceType);
|
|
4898
|
+
}
|
|
4899
|
+
});
|
|
4900
|
+
});
|
|
4901
|
+
document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
|
|
4902
|
+
document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
|
|
4903
|
+
document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
|
|
4904
|
+
document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
|
|
4905
|
+
document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
|
|
4906
|
+
document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
|
|
4907
|
+
if (e.key === "Enter" && this.wsStatus !== "connected") {
|
|
4908
|
+
this.handleConnect();
|
|
4909
|
+
}
|
|
4910
|
+
});
|
|
4911
|
+
return this.sourceModal;
|
|
4912
|
+
}
|
|
4913
|
+
selectSourceType(type, skipUpdate = false) {
|
|
4914
|
+
if (!this.sourceModal) return;
|
|
4915
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4916
|
+
if (btn.getAttribute("data-source") === type) {
|
|
4917
|
+
btn.classList.add("active");
|
|
4918
|
+
} else {
|
|
4919
|
+
btn.classList.remove("active");
|
|
4920
|
+
}
|
|
4921
|
+
});
|
|
4922
|
+
this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
|
|
4923
|
+
if (controls.getAttribute("data-source") === type) {
|
|
4924
|
+
controls.classList.add("active");
|
|
4925
|
+
} else {
|
|
4926
|
+
controls.classList.remove("active");
|
|
4927
|
+
}
|
|
4928
|
+
});
|
|
4929
|
+
if (!skipUpdate) {
|
|
4930
|
+
this.updateSourceModalContent();
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
updateSourceModalContent() {
|
|
4934
|
+
if (!this.sourceModal) return;
|
|
4935
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
4936
|
+
const connectWsBtn = document.getElementById("source-modal-connect-ws");
|
|
4937
|
+
const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
|
|
4938
|
+
const changeWsBtn = document.getElementById("source-modal-change-ws");
|
|
4939
|
+
if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
|
|
4940
|
+
const isWsConnected = this.wsStatus === "connected";
|
|
4941
|
+
const isWsConnecting = this.wsStatus === "connecting";
|
|
4942
|
+
connectWsBtn.disabled = isWsConnected || isWsConnecting;
|
|
4943
|
+
disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4944
|
+
changeWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4945
|
+
urlInput.disabled = isWsConnecting;
|
|
4946
|
+
}
|
|
4947
|
+
const connectFolderBtn = document.getElementById("source-modal-connect-folder");
|
|
4948
|
+
const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
|
|
4949
|
+
if (connectFolderBtn && disconnectFolderBtn) {
|
|
4950
|
+
const isFolderConnected = this.fsStatus === "connected";
|
|
4951
|
+
const isFolderOpening = this.fsStatus === "opening";
|
|
4952
|
+
connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
|
|
4953
|
+
disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
|
|
4954
|
+
}
|
|
4955
|
+
const statusDiv = document.getElementById("source-modal-status");
|
|
4956
|
+
if (!statusDiv) return;
|
|
4957
|
+
const currentUrl = urlInput?.value || this.wsUrl;
|
|
4958
|
+
statusDiv.innerHTML = "";
|
|
4959
|
+
if (this.activeSourceType === "ws") {
|
|
4960
|
+
if (this.wsStatus === "connecting") {
|
|
4961
|
+
statusDiv.innerHTML = `
|
|
4962
|
+
<div class="status-message info">
|
|
4963
|
+
<span class="loading-spinner"></span>
|
|
4964
|
+
Connecting to ${currentUrl}...
|
|
4965
|
+
</div>
|
|
4966
|
+
`;
|
|
4967
|
+
} else if (this.wsStatus === "connected") {
|
|
4968
|
+
statusDiv.innerHTML = `
|
|
4969
|
+
<div class="status-message success">
|
|
4970
|
+
\u2713 Connected to ${currentUrl}
|
|
4971
|
+
</div>
|
|
4972
|
+
`;
|
|
4973
|
+
} else if (this.wsStatus === "error") {
|
|
4974
|
+
statusDiv.innerHTML = `
|
|
4975
|
+
<div class="status-message error">
|
|
4976
|
+
\u2717 Connection error. Please check the URL and try again.
|
|
4977
|
+
</div>
|
|
4978
|
+
`;
|
|
4979
|
+
} else {
|
|
4980
|
+
statusDiv.innerHTML = `
|
|
4981
|
+
<div class="status-message info">
|
|
4982
|
+
Not connected
|
|
4983
|
+
</div>
|
|
4984
|
+
`;
|
|
4985
|
+
}
|
|
4986
|
+
} else if (this.activeSourceType === "folder") {
|
|
4987
|
+
if (this.fsStatus === "opening") {
|
|
4988
|
+
statusDiv.innerHTML = `
|
|
4989
|
+
<div class="status-message info">
|
|
4990
|
+
<span class="loading-spinner"></span>
|
|
4991
|
+
Opening folder...
|
|
4992
|
+
</div>
|
|
4993
|
+
`;
|
|
4994
|
+
} else if (this.fsStatus === "connected") {
|
|
4995
|
+
statusDiv.innerHTML = `
|
|
4996
|
+
<div class="status-message success">
|
|
4997
|
+
\u2713 Folder connected and watching for changes
|
|
4998
|
+
</div>
|
|
4999
|
+
`;
|
|
5000
|
+
} else if (this.fsStatus === "error") {
|
|
5001
|
+
statusDiv.innerHTML = `
|
|
5002
|
+
<div class="status-message error">
|
|
5003
|
+
\u2717 Error opening folder. Please try again.
|
|
5004
|
+
</div>
|
|
5005
|
+
`;
|
|
5006
|
+
} else {
|
|
5007
|
+
statusDiv.innerHTML = `
|
|
5008
|
+
<div class="status-message info">
|
|
5009
|
+
Not connected
|
|
5010
|
+
</div>
|
|
5011
|
+
`;
|
|
5012
|
+
}
|
|
5013
|
+
} else if (this.activeSourceType === "file") {
|
|
5014
|
+
const example = this.options.examples[this.currentExample];
|
|
5015
|
+
const filePath = example.source?.type === "file" ? example.source.path : "";
|
|
5016
|
+
if (this.fileStatus === "connecting") {
|
|
5017
|
+
statusDiv.innerHTML = `
|
|
5018
|
+
<div class="status-message info">
|
|
5019
|
+
<span class="loading-spinner"></span>
|
|
5020
|
+
Connecting to ${filePath}...
|
|
5021
|
+
</div>
|
|
5022
|
+
`;
|
|
5023
|
+
} else if (this.fileStatus === "connected") {
|
|
5024
|
+
statusDiv.innerHTML = `
|
|
5025
|
+
<div class="status-message success">
|
|
5026
|
+
\u2713 Connected to ${filePath}
|
|
5027
|
+
</div>
|
|
5028
|
+
`;
|
|
5029
|
+
} else if (this.fileStatus === "error") {
|
|
5030
|
+
statusDiv.innerHTML = `
|
|
5031
|
+
<div class="status-message error">
|
|
5032
|
+
\u2717 Error loading file. Please check the path and try again.
|
|
5033
|
+
</div>
|
|
5034
|
+
`;
|
|
5035
|
+
} else {
|
|
5036
|
+
statusDiv.innerHTML = `
|
|
5037
|
+
<div class="status-message info">
|
|
5038
|
+
Not connected
|
|
5039
|
+
</div>
|
|
5040
|
+
`;
|
|
5041
|
+
}
|
|
5042
|
+
} else {
|
|
5043
|
+
statusDiv.innerHTML = `
|
|
5044
|
+
<div class="status-message info">
|
|
5045
|
+
Select a source type to connect
|
|
5046
|
+
</div>
|
|
5047
|
+
`;
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
updateSourceModal() {
|
|
5051
|
+
if (!this.sourceModal) return;
|
|
5052
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5053
|
+
this.selectSourceType(activeType, true);
|
|
5054
|
+
this.updateSourceModalContent();
|
|
5055
|
+
}
|
|
5056
|
+
openSourceModal() {
|
|
5057
|
+
this.createSourceModal();
|
|
5058
|
+
if (this.sourceModal) {
|
|
5059
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5060
|
+
if (urlInput) {
|
|
5061
|
+
urlInput.value = this.wsUrl;
|
|
5062
|
+
}
|
|
5063
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5064
|
+
this.selectSourceType(activeType);
|
|
5065
|
+
this.updateSourceModal();
|
|
5066
|
+
this.sourceModal.style.display = "flex";
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
closeSourceModal() {
|
|
5070
|
+
if (this.sourceModal) {
|
|
5071
|
+
this.sourceModal.style.display = "none";
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
handleConnect() {
|
|
5075
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5076
|
+
if (!urlInput) return;
|
|
5077
|
+
const url = urlInput.value.trim() || "ws://localhost:8787";
|
|
5078
|
+
this.wsUrl = url;
|
|
5079
|
+
if (this.wsSource) {
|
|
5080
|
+
this.wsSource.disconnect();
|
|
5081
|
+
}
|
|
5082
|
+
this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
5083
|
+
this.wsSource.connect();
|
|
5084
|
+
this.updateSourceModal();
|
|
5085
|
+
}
|
|
5086
|
+
handleDisconnect() {
|
|
5087
|
+
this.wsSource?.disconnect();
|
|
5088
|
+
this.updateSourceModal();
|
|
5089
|
+
}
|
|
5090
|
+
handleChangeConnection() {
|
|
5091
|
+
if (this.wsSource) {
|
|
5092
|
+
this.wsSource.disconnect();
|
|
5093
|
+
}
|
|
5094
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5095
|
+
if (urlInput) {
|
|
5096
|
+
urlInput.focus();
|
|
5097
|
+
urlInput.select();
|
|
5098
|
+
}
|
|
5099
|
+
this.updateSourceModal();
|
|
5100
|
+
}
|
|
5101
|
+
async handleOpenFolder() {
|
|
5102
|
+
if (!this.fsSource) {
|
|
5103
|
+
this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
|
|
5104
|
+
}
|
|
5105
|
+
this.updateSourceModal();
|
|
5106
|
+
await this.fsSource.openDirectory();
|
|
5107
|
+
}
|
|
5108
|
+
handleCloseFolder() {
|
|
5109
|
+
this.fsSource?.close();
|
|
5110
|
+
this.updateSourceModal();
|
|
5111
|
+
}
|
|
3001
5112
|
};
|
|
3002
5113
|
|
|
3003
5114
|
// src/index.ts
|
|
@@ -3009,5 +5120,10 @@ async function graph(args = { root: "app" }) {
|
|
|
3009
5120
|
var index_default = graph;
|
|
3010
5121
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3011
5122
|
0 && (module.exports = {
|
|
5123
|
+
FileSource,
|
|
5124
|
+
FileSystemSource,
|
|
5125
|
+
Ingest,
|
|
5126
|
+
Playground,
|
|
5127
|
+
WebSocketSource,
|
|
3012
5128
|
graph
|
|
3013
5129
|
});
|