@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.js
CHANGED
|
@@ -3,6 +3,70 @@ import { Map as IMap, List as IList, Set as ISet6 } from "immutable";
|
|
|
3
3
|
|
|
4
4
|
// src/graph/node.ts
|
|
5
5
|
import { Record, Set as ISet } from "immutable";
|
|
6
|
+
|
|
7
|
+
// src/log.ts
|
|
8
|
+
import pino from "pino";
|
|
9
|
+
var resolvedLevel = typeof globalThis !== "undefined" && globalThis.__LOG_LEVEL || typeof process !== "undefined" && process.env?.LOG_LEVEL || "debug";
|
|
10
|
+
var browserOpts = { asObject: true };
|
|
11
|
+
browserOpts.transmit = {
|
|
12
|
+
level: resolvedLevel,
|
|
13
|
+
send: (level, log12) => {
|
|
14
|
+
try {
|
|
15
|
+
const endpoint = typeof globalThis !== "undefined" && globalThis.__LOG_INGEST_URL || typeof process !== "undefined" && process.env?.LOG_INGEST_URL || void 0;
|
|
16
|
+
if (!endpoint || typeof window === "undefined") {
|
|
17
|
+
try {
|
|
18
|
+
console.debug("[graph-core] transmit skipped", { endpoint, hasWindow: typeof window !== "undefined", level });
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const line = JSON.stringify({ level, ...log12, ts: Date.now() }) + "\n";
|
|
24
|
+
try {
|
|
25
|
+
console.debug("[graph-core] transmit sending", { endpoint, level, bytes: line.length });
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
fetch(endpoint, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
mode: "no-cors",
|
|
31
|
+
credentials: "omit",
|
|
32
|
+
body: line,
|
|
33
|
+
keepalive: true
|
|
34
|
+
}).then(() => {
|
|
35
|
+
try {
|
|
36
|
+
console.debug("[graph-core] transmit fetch ok");
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}).catch((err) => {
|
|
40
|
+
try {
|
|
41
|
+
console.debug("[graph-core] transmit fetch error", err?.message || err);
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
} catch (e) {
|
|
46
|
+
try {
|
|
47
|
+
console.debug("[graph-core] transmit error", e?.message || e);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var base = pino({
|
|
54
|
+
level: resolvedLevel,
|
|
55
|
+
browser: browserOpts
|
|
56
|
+
});
|
|
57
|
+
function logger(module) {
|
|
58
|
+
const child = base.child({ module });
|
|
59
|
+
return {
|
|
60
|
+
error: (msg, ...args) => child.error({ args }, msg),
|
|
61
|
+
warn: (msg, ...args) => child.warn({ args }, msg),
|
|
62
|
+
info: (msg, ...args) => child.info({ args }, msg),
|
|
63
|
+
debug: (msg, ...args) => child.debug({ args }, msg)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
var log = logger("core");
|
|
67
|
+
|
|
68
|
+
// src/graph/node.ts
|
|
69
|
+
var log2 = logger("node");
|
|
6
70
|
var defNodeData = {
|
|
7
71
|
id: "",
|
|
8
72
|
data: void 0,
|
|
@@ -11,7 +75,7 @@ var defNodeData = {
|
|
|
11
75
|
text: void 0,
|
|
12
76
|
type: void 0,
|
|
13
77
|
render: void 0,
|
|
14
|
-
ports: {
|
|
78
|
+
ports: {},
|
|
15
79
|
aligned: {},
|
|
16
80
|
edges: { in: ISet(), out: ISet() },
|
|
17
81
|
segs: { in: ISet(), out: ISet() },
|
|
@@ -45,10 +109,7 @@ var Node = class _Node extends Record(defNodeData) {
|
|
|
45
109
|
return this.isDummy ? this.id : _Node.key(this);
|
|
46
110
|
}
|
|
47
111
|
static key(node) {
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
static isDummyId(nodeId) {
|
|
51
|
-
return nodeId.startsWith(_Node.dummyPrefix);
|
|
112
|
+
return `k:${node.id}:${node.version}`;
|
|
52
113
|
}
|
|
53
114
|
static addNormal(g, data) {
|
|
54
115
|
const layer = g.layerAt(0);
|
|
@@ -58,7 +119,9 @@ var Node = class _Node extends Record(defNodeData) {
|
|
|
58
119
|
segs: { in: ISet(), out: ISet() },
|
|
59
120
|
aligned: {},
|
|
60
121
|
edgeIds: [],
|
|
61
|
-
layerId: layer.id
|
|
122
|
+
layerId: layer.id,
|
|
123
|
+
lpos: void 0,
|
|
124
|
+
pos: void 0
|
|
62
125
|
});
|
|
63
126
|
layer.addNode(g, node.id);
|
|
64
127
|
g.nodes.set(node.id, node);
|
|
@@ -121,6 +184,18 @@ var Node = class _Node extends Record(defNodeData) {
|
|
|
121
184
|
getLayer(g) {
|
|
122
185
|
return g.getLayer(this.layerId);
|
|
123
186
|
}
|
|
187
|
+
margin(g) {
|
|
188
|
+
return this.isDummy ? g.options.edgeSpacing - g.options.dummyNodeSize : g.options.nodeMargin;
|
|
189
|
+
}
|
|
190
|
+
marginWith(g, other) {
|
|
191
|
+
return Math.max(this.margin(g), other.margin(g));
|
|
192
|
+
}
|
|
193
|
+
width(g) {
|
|
194
|
+
return this.dims?.[g.w] ?? 0;
|
|
195
|
+
}
|
|
196
|
+
right(g) {
|
|
197
|
+
return this.lpos + this.width(g);
|
|
198
|
+
}
|
|
124
199
|
setIndex(g, index) {
|
|
125
200
|
if (this.index == index) return this;
|
|
126
201
|
return this.mut(g).set("index", index);
|
|
@@ -167,9 +242,22 @@ var Node = class _Node extends Record(defNodeData) {
|
|
|
167
242
|
return node;
|
|
168
243
|
}
|
|
169
244
|
delSelf(g) {
|
|
245
|
+
if (this.aligned.in)
|
|
246
|
+
g.getNode(this.aligned.in).setAligned(g, "out", void 0);
|
|
247
|
+
if (this.aligned.out)
|
|
248
|
+
g.getNode(this.aligned.out).setAligned(g, "in", void 0);
|
|
170
249
|
this.getLayer(g).delNode(g, this.id);
|
|
171
|
-
for (const
|
|
172
|
-
|
|
250
|
+
for (const edge of this.rels(g, "edges", "both"))
|
|
251
|
+
edge.delSelf(g);
|
|
252
|
+
const remainingSegIds = [...this.segs.in, ...this.segs.out];
|
|
253
|
+
if (remainingSegIds.length > 0) {
|
|
254
|
+
for (const segId of remainingSegIds) {
|
|
255
|
+
if (g.segs?.has?.(segId)) {
|
|
256
|
+
g.getSeg(segId).delSelf(g);
|
|
257
|
+
} else {
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
173
261
|
g.nodes.delete(this.id);
|
|
174
262
|
g.dirtyNodes.delete(this.id);
|
|
175
263
|
g.delNodes.add(this.id);
|
|
@@ -262,30 +350,7 @@ var Node = class _Node extends Record(defNodeData) {
|
|
|
262
350
|
|
|
263
351
|
// src/graph/edge.ts
|
|
264
352
|
import { Record as Record2 } from "immutable";
|
|
265
|
-
|
|
266
|
-
// src/log.ts
|
|
267
|
-
var levels = {
|
|
268
|
-
error: 0,
|
|
269
|
-
warn: 1,
|
|
270
|
-
info: 2,
|
|
271
|
-
debug: 3
|
|
272
|
-
};
|
|
273
|
-
var currentLevel = "debug";
|
|
274
|
-
function shouldLog(level) {
|
|
275
|
-
return levels[level] <= levels[currentLevel];
|
|
276
|
-
}
|
|
277
|
-
function logger(module) {
|
|
278
|
-
return {
|
|
279
|
-
error: (msg, ...args) => shouldLog("error") && console.error(`[${module}] ${msg}`, ...args),
|
|
280
|
-
warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module}] ${msg}`, ...args),
|
|
281
|
-
info: (msg, ...args) => shouldLog("info") && console.info(`[${module}] ${msg}`, ...args),
|
|
282
|
-
debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module}] ${msg}`, ...args)
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
var log = logger("core");
|
|
286
|
-
|
|
287
|
-
// src/graph/edge.ts
|
|
288
|
-
var log2 = logger("edge");
|
|
353
|
+
var log3 = logger("edge");
|
|
289
354
|
var defEdgeData = {
|
|
290
355
|
id: "",
|
|
291
356
|
data: null,
|
|
@@ -293,7 +358,6 @@ var defEdgeData = {
|
|
|
293
358
|
source: { id: "" },
|
|
294
359
|
target: { id: "" },
|
|
295
360
|
type: void 0,
|
|
296
|
-
style: void 0,
|
|
297
361
|
mutable: false,
|
|
298
362
|
segIds: []
|
|
299
363
|
};
|
|
@@ -374,7 +438,7 @@ var Edge = class _Edge extends Record2(defEdgeData) {
|
|
|
374
438
|
source = edge.source.id;
|
|
375
439
|
if (edge.source?.port)
|
|
376
440
|
source = `${source}.${edge.source.port}`;
|
|
377
|
-
const marker = edge.source?.marker
|
|
441
|
+
const marker = edge.source?.marker;
|
|
378
442
|
if (marker && marker != "none") source += `[${marker}]`;
|
|
379
443
|
source += "-";
|
|
380
444
|
}
|
|
@@ -384,7 +448,7 @@ var Edge = class _Edge extends Record2(defEdgeData) {
|
|
|
384
448
|
if (edge.target.port)
|
|
385
449
|
target = `${target}.${edge.target.port}`;
|
|
386
450
|
target = "-" + target;
|
|
387
|
-
const marker = edge.target?.marker ??
|
|
451
|
+
const marker = edge.target?.marker ?? "arrow";
|
|
388
452
|
if (marker && marker != "none") target += `[${marker}]`;
|
|
389
453
|
}
|
|
390
454
|
const type = edge.type || "";
|
|
@@ -426,7 +490,6 @@ var defSegData = {
|
|
|
426
490
|
source: { id: "" },
|
|
427
491
|
target: { id: "" },
|
|
428
492
|
type: void 0,
|
|
429
|
-
style: void 0,
|
|
430
493
|
edgeIds: ISet2(),
|
|
431
494
|
trackPos: void 0,
|
|
432
495
|
svg: void 0,
|
|
@@ -457,7 +520,7 @@ var Seg = class _Seg extends Record3(defSegData) {
|
|
|
457
520
|
sameEnd(other, side) {
|
|
458
521
|
const mine = this[side];
|
|
459
522
|
const yours = other[side];
|
|
460
|
-
return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
|
|
523
|
+
return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker && this.type === other.type;
|
|
461
524
|
}
|
|
462
525
|
setPos(g, source, target) {
|
|
463
526
|
return this.mut(g).merge({
|
|
@@ -529,7 +592,7 @@ var Seg = class _Seg extends Record3(defSegData) {
|
|
|
529
592
|
|
|
530
593
|
// src/graph/layer.ts
|
|
531
594
|
import { Record as Record4, Set as ISet3 } from "immutable";
|
|
532
|
-
var
|
|
595
|
+
var log4 = logger("layer");
|
|
533
596
|
var defLayerData = {
|
|
534
597
|
id: "",
|
|
535
598
|
index: 0,
|
|
@@ -554,7 +617,7 @@ var Layer = class extends Record4(defLayerData) {
|
|
|
554
617
|
mutable: false
|
|
555
618
|
}).asImmutable();
|
|
556
619
|
}
|
|
557
|
-
get
|
|
620
|
+
get nodeCount() {
|
|
558
621
|
return this.nodeIds.size;
|
|
559
622
|
}
|
|
560
623
|
*nodes(g) {
|
|
@@ -609,9 +672,8 @@ var Layer = class extends Record4(defLayerData) {
|
|
|
609
672
|
reindex(g, nodeId) {
|
|
610
673
|
if (!this.isSorted) return void 0;
|
|
611
674
|
const sorted = this.sorted.filter((id) => id != nodeId);
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
g.getNode(sorted[i]).setIndex(g, i);
|
|
675
|
+
for (const [i, id] of this.sorted.entries())
|
|
676
|
+
g.getNode(id).setIndex(g, i);
|
|
615
677
|
return sorted;
|
|
616
678
|
}
|
|
617
679
|
delNode(g, nodeId) {
|
|
@@ -783,12 +845,12 @@ var Cycles = class _Cycles {
|
|
|
783
845
|
|
|
784
846
|
// src/graph/services/dummy.ts
|
|
785
847
|
import { Set as ISet4 } from "immutable";
|
|
786
|
-
var
|
|
848
|
+
var log5 = logger("dummy");
|
|
787
849
|
var Dummy = class _Dummy {
|
|
788
850
|
static updateDummies(g) {
|
|
789
851
|
for (const edgeId of g.dirtyEdges) {
|
|
790
852
|
const edge = g.getEdge(edgeId);
|
|
791
|
-
const { type
|
|
853
|
+
const { type } = edge;
|
|
792
854
|
const sourceLayer = edge.sourceNode(g).layerIndex(g);
|
|
793
855
|
const targetLayer = edge.targetNode(g).layerIndex(g);
|
|
794
856
|
let segIndex = 0;
|
|
@@ -812,7 +874,7 @@ var Dummy = class _Dummy {
|
|
|
812
874
|
});
|
|
813
875
|
target = { id: dummy.id };
|
|
814
876
|
}
|
|
815
|
-
seg = Seg.add(g, { source, target, type,
|
|
877
|
+
seg = Seg.add(g, { source, target, type, edgeIds: ISet4([edgeId]) });
|
|
816
878
|
segs.splice(segIndex, 0, seg.id);
|
|
817
879
|
changed = true;
|
|
818
880
|
} else if (segLayer < layerIndex || seg.source.id != source.id || seg.source.port != source.port || layerIndex == targetLayer && (seg.target.id != edge.target.id || seg.target.port != edge.target.port)) {
|
|
@@ -851,9 +913,8 @@ var Dummy = class _Dummy {
|
|
|
851
913
|
let layer = g.getLayer(layerId);
|
|
852
914
|
const groups = /* @__PURE__ */ new Map();
|
|
853
915
|
for (const nodeId of layer.nodeIds) {
|
|
854
|
-
if (!Node.isDummyId(nodeId)) continue;
|
|
855
916
|
const node = g.getNode(nodeId);
|
|
856
|
-
if (node.isMerged) continue;
|
|
917
|
+
if (!node.isDummy || node.isMerged) continue;
|
|
857
918
|
const edge = g.getEdge(node.edgeIds[0]);
|
|
858
919
|
const key = Edge.key(edge, "k:", side);
|
|
859
920
|
if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
|
|
@@ -861,7 +922,7 @@ var Dummy = class _Dummy {
|
|
|
861
922
|
}
|
|
862
923
|
for (const [key, group] of groups) {
|
|
863
924
|
if (group.size == 1) continue;
|
|
864
|
-
const edgeIds = [...group].map((node) => node.
|
|
925
|
+
const edgeIds = [...group].map((node) => node.edgeIds[0]);
|
|
865
926
|
const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
|
|
866
927
|
let seg;
|
|
867
928
|
for (const old of group) {
|
|
@@ -871,8 +932,9 @@ var Dummy = class _Dummy {
|
|
|
871
932
|
const example = g.getSeg(segId);
|
|
872
933
|
seg = Seg.add(g, {
|
|
873
934
|
...example,
|
|
874
|
-
edgeIds: ISet4(
|
|
875
|
-
[
|
|
935
|
+
edgeIds: ISet4(edgeIds),
|
|
936
|
+
[side]: { ...example[side] },
|
|
937
|
+
[altSide]: { ...example[altSide], id: dummy.id, port: void 0 }
|
|
876
938
|
});
|
|
877
939
|
}
|
|
878
940
|
edge = edge.replaceSegId(g, segId, seg.id);
|
|
@@ -884,12 +946,15 @@ var Dummy = class _Dummy {
|
|
|
884
946
|
const example = g.getSeg(segId);
|
|
885
947
|
const seg2 = Seg.add(g, {
|
|
886
948
|
...example,
|
|
887
|
-
edgeIds: ISet4([old.
|
|
888
|
-
[
|
|
949
|
+
edgeIds: ISet4([old.edgeIds[0]]),
|
|
950
|
+
[altSide]: { ...example[altSide] },
|
|
951
|
+
[side]: { ...example[side], id: dummy.id, port: void 0 }
|
|
889
952
|
});
|
|
890
953
|
edge = edge.replaceSegId(g, segId, seg2.id);
|
|
891
954
|
}
|
|
892
955
|
}
|
|
956
|
+
for (const old of group)
|
|
957
|
+
old.delSelf(g);
|
|
893
958
|
}
|
|
894
959
|
}
|
|
895
960
|
}
|
|
@@ -897,7 +962,7 @@ var Dummy = class _Dummy {
|
|
|
897
962
|
|
|
898
963
|
// src/graph/services/layers.ts
|
|
899
964
|
import { Seq } from "immutable";
|
|
900
|
-
var
|
|
965
|
+
var log6 = logger("layers");
|
|
901
966
|
var Layers = class {
|
|
902
967
|
static updateLayers(g) {
|
|
903
968
|
const stack = [...g.dirtyNodes].map((id) => g.getNode(id)).filter((node) => !node.isDummy).sort((a, b) => b.layerIndex(g) - a.layerIndex(g)).map((node) => node.id);
|
|
@@ -960,7 +1025,7 @@ var Layers = class {
|
|
|
960
1025
|
|
|
961
1026
|
// src/graph/services/layout.ts
|
|
962
1027
|
import { Seq as Seq2 } from "immutable";
|
|
963
|
-
var
|
|
1028
|
+
var log7 = logger("layout");
|
|
964
1029
|
var Layout = class _Layout {
|
|
965
1030
|
static parentIndex(g, node) {
|
|
966
1031
|
const parents = Seq2([...node.adjs(g, "segs", "in")]);
|
|
@@ -977,8 +1042,8 @@ var Layout = class _Layout {
|
|
|
977
1042
|
if (a.isDummy && !b.isDummy) return -1;
|
|
978
1043
|
if (!a.isDummy && b.isDummy) return 1;
|
|
979
1044
|
if (!a.isDummy) return a.id.localeCompare(b.id);
|
|
980
|
-
const minA =
|
|
981
|
-
const minB =
|
|
1045
|
+
const minA = Seq2(a.edgeIds).min();
|
|
1046
|
+
const minB = Seq2(b.edgeIds).min();
|
|
982
1047
|
return minA.localeCompare(minB);
|
|
983
1048
|
}
|
|
984
1049
|
static positionNodes(g) {
|
|
@@ -1004,7 +1069,12 @@ var Layout = class _Layout {
|
|
|
1004
1069
|
let node = g.getNode(sorted[i]);
|
|
1005
1070
|
node = node.setIndex(g, i).setLayerPos(g, lpos);
|
|
1006
1071
|
const size = node.dims?.[g.w] ?? 0;
|
|
1007
|
-
|
|
1072
|
+
let margin = node.margin(g);
|
|
1073
|
+
if (i + 1 < sorted.length) {
|
|
1074
|
+
const next = g.getNode(sorted[i + 1]);
|
|
1075
|
+
margin = node.marginWith(g, next);
|
|
1076
|
+
}
|
|
1077
|
+
lpos += size + margin;
|
|
1008
1078
|
}
|
|
1009
1079
|
}
|
|
1010
1080
|
}
|
|
@@ -1036,7 +1106,7 @@ var Layout = class _Layout {
|
|
|
1036
1106
|
let iterations = 0;
|
|
1037
1107
|
while (true) {
|
|
1038
1108
|
if (++iterations > 10) {
|
|
1039
|
-
|
|
1109
|
+
log7.error(`alignNodes: infinite loop detected in layer ${layerId}`);
|
|
1040
1110
|
break;
|
|
1041
1111
|
}
|
|
1042
1112
|
let changed = false;
|
|
@@ -1096,7 +1166,7 @@ var Layout = class _Layout {
|
|
|
1096
1166
|
static anchorPos(g, seg, side) {
|
|
1097
1167
|
const nodeId = seg[side].id;
|
|
1098
1168
|
const node = g.getNode(nodeId);
|
|
1099
|
-
let p = {
|
|
1169
|
+
let p = { [g.x]: node.lpos, [g.y]: node.pos?.[g.y] ?? 0 };
|
|
1100
1170
|
let w = node.dims?.[g.w] ?? 0;
|
|
1101
1171
|
let h = node.dims?.[g.h] ?? 0;
|
|
1102
1172
|
if (node.isDummy)
|
|
@@ -1142,22 +1212,18 @@ var Layout = class _Layout {
|
|
|
1142
1212
|
}
|
|
1143
1213
|
static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
|
|
1144
1214
|
const node = g.getNode(nodeId);
|
|
1145
|
-
log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
|
|
1146
1215
|
if (!conservative)
|
|
1147
1216
|
_Layout.markAligned(g, nodeId, alignId, dir, lpos);
|
|
1148
|
-
const
|
|
1149
|
-
const nodeWidth = node.dims?.[g.w] ?? 0;
|
|
1150
|
-
const aMin = lpos - space, aMax = lpos + nodeWidth + space;
|
|
1217
|
+
const nodeRight = lpos + node.width(g);
|
|
1151
1218
|
repeat:
|
|
1152
1219
|
for (const otherId of node.getLayer(g).nodeIds) {
|
|
1153
1220
|
if (otherId == nodeId) continue;
|
|
1154
1221
|
const other = g.getNode(otherId);
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
if (aMin < bMax && bMin < aMax) {
|
|
1222
|
+
const margin = node.marginWith(g, other);
|
|
1223
|
+
const gap = lpos < other.lpos ? other.lpos - nodeRight : lpos - other.right(g);
|
|
1224
|
+
if (gap < margin) {
|
|
1159
1225
|
if (conservative) return false;
|
|
1160
|
-
const safePos = reverseMove ?
|
|
1226
|
+
const safePos = reverseMove ? lpos - other.width(g) - margin : nodeRight + margin;
|
|
1161
1227
|
_Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
|
|
1162
1228
|
continue repeat;
|
|
1163
1229
|
}
|
|
@@ -1206,11 +1272,12 @@ var Layout = class _Layout {
|
|
|
1206
1272
|
for (const layerId of g.layerList) {
|
|
1207
1273
|
const layer = g.getLayer(layerId);
|
|
1208
1274
|
if (layer.sorted.length < 2) continue;
|
|
1209
|
-
for (const nodeId of layer.sorted) {
|
|
1275
|
+
for (const [i, nodeId] of layer.sorted.entries()) {
|
|
1210
1276
|
const node = g.getNode(nodeId);
|
|
1211
1277
|
if (node.index == 0) continue;
|
|
1212
1278
|
let minGap = Infinity;
|
|
1213
1279
|
const stack = [];
|
|
1280
|
+
let maxMargin = 0;
|
|
1214
1281
|
for (const right of _Layout.aligned(g, nodeId, "both")) {
|
|
1215
1282
|
stack.push(right);
|
|
1216
1283
|
const leftId = _Layout.leftOf(g, right);
|
|
@@ -1219,8 +1286,10 @@ var Layout = class _Layout {
|
|
|
1219
1286
|
const leftWidth = left.dims?.[g.w] ?? 0;
|
|
1220
1287
|
const gap = right.lpos - left.lpos - leftWidth;
|
|
1221
1288
|
if (gap < minGap) minGap = gap;
|
|
1289
|
+
const margin = right.marginWith(g, left);
|
|
1290
|
+
if (margin > maxMargin) maxMargin = margin;
|
|
1222
1291
|
}
|
|
1223
|
-
const delta = minGap -
|
|
1292
|
+
const delta = minGap - maxMargin;
|
|
1224
1293
|
if (delta <= 0) continue;
|
|
1225
1294
|
anyChanged = true;
|
|
1226
1295
|
for (const right of stack)
|
|
@@ -1268,9 +1337,8 @@ var Layout = class _Layout {
|
|
|
1268
1337
|
};
|
|
1269
1338
|
|
|
1270
1339
|
// src/canvas/marker.tsx
|
|
1271
|
-
import { default as default2 } from "./marker.css?raw";
|
|
1272
1340
|
import { jsx } from "jsx-dom/jsx-runtime";
|
|
1273
|
-
function arrow(size,
|
|
1341
|
+
function arrow(size, reverse = false) {
|
|
1274
1342
|
const h = size / 1.5;
|
|
1275
1343
|
const w = size;
|
|
1276
1344
|
const ry = h / 2;
|
|
@@ -1279,7 +1347,7 @@ function arrow(size, classPrefix, reverse = false) {
|
|
|
1279
1347
|
"marker",
|
|
1280
1348
|
{
|
|
1281
1349
|
id: `g3p-marker-arrow${suffix}`,
|
|
1282
|
-
className:
|
|
1350
|
+
className: "g3p-marker g3p-marker-arrow",
|
|
1283
1351
|
markerWidth: size,
|
|
1284
1352
|
markerHeight: size,
|
|
1285
1353
|
refX: "2",
|
|
@@ -1290,7 +1358,7 @@ function arrow(size, classPrefix, reverse = false) {
|
|
|
1290
1358
|
}
|
|
1291
1359
|
);
|
|
1292
1360
|
}
|
|
1293
|
-
function circle(size,
|
|
1361
|
+
function circle(size, reverse = false) {
|
|
1294
1362
|
const r = size / 3;
|
|
1295
1363
|
const cy = size / 2;
|
|
1296
1364
|
const suffix = reverse ? "-reverse" : "";
|
|
@@ -1298,7 +1366,7 @@ function circle(size, classPrefix, reverse = false) {
|
|
|
1298
1366
|
"marker",
|
|
1299
1367
|
{
|
|
1300
1368
|
id: `g3p-marker-circle${suffix}`,
|
|
1301
|
-
className:
|
|
1369
|
+
className: "g3p-marker g3p-marker-circle",
|
|
1302
1370
|
markerWidth: size,
|
|
1303
1371
|
markerHeight: size,
|
|
1304
1372
|
refX: "2",
|
|
@@ -1309,7 +1377,7 @@ function circle(size, classPrefix, reverse = false) {
|
|
|
1309
1377
|
}
|
|
1310
1378
|
);
|
|
1311
1379
|
}
|
|
1312
|
-
function diamond(size,
|
|
1380
|
+
function diamond(size, reverse = false) {
|
|
1313
1381
|
const w = size * 0.7;
|
|
1314
1382
|
const h = size / 2;
|
|
1315
1383
|
const cy = size / 2;
|
|
@@ -1318,7 +1386,7 @@ function diamond(size, classPrefix, reverse = false) {
|
|
|
1318
1386
|
"marker",
|
|
1319
1387
|
{
|
|
1320
1388
|
id: `g3p-marker-diamond${suffix}`,
|
|
1321
|
-
className:
|
|
1389
|
+
className: "g3p-marker g3p-marker-diamond",
|
|
1322
1390
|
markerWidth: size,
|
|
1323
1391
|
markerHeight: size,
|
|
1324
1392
|
refX: "2",
|
|
@@ -1329,7 +1397,7 @@ function diamond(size, classPrefix, reverse = false) {
|
|
|
1329
1397
|
}
|
|
1330
1398
|
);
|
|
1331
1399
|
}
|
|
1332
|
-
function bar(size,
|
|
1400
|
+
function bar(size, reverse = false) {
|
|
1333
1401
|
const h = size * 0.6;
|
|
1334
1402
|
const cy = size / 2;
|
|
1335
1403
|
const suffix = reverse ? "-reverse" : "";
|
|
@@ -1337,7 +1405,7 @@ function bar(size, classPrefix, reverse = false) {
|
|
|
1337
1405
|
"marker",
|
|
1338
1406
|
{
|
|
1339
1407
|
id: `g3p-marker-bar${suffix}`,
|
|
1340
|
-
className:
|
|
1408
|
+
className: "g3p-marker g3p-marker-bar",
|
|
1341
1409
|
markerWidth: size,
|
|
1342
1410
|
markerHeight: size,
|
|
1343
1411
|
refX: "2",
|
|
@@ -1348,7 +1416,7 @@ function bar(size, classPrefix, reverse = false) {
|
|
|
1348
1416
|
}
|
|
1349
1417
|
);
|
|
1350
1418
|
}
|
|
1351
|
-
function none(size,
|
|
1419
|
+
function none(size, reverse = false) {
|
|
1352
1420
|
return void 0;
|
|
1353
1421
|
}
|
|
1354
1422
|
function normalize(data) {
|
|
@@ -1367,7 +1435,7 @@ var markerDefs = {
|
|
|
1367
1435
|
};
|
|
1368
1436
|
|
|
1369
1437
|
// src/graph/services/lines.ts
|
|
1370
|
-
var
|
|
1438
|
+
var log8 = logger("lines");
|
|
1371
1439
|
var Lines = class _Lines {
|
|
1372
1440
|
static layoutSeg(g, seg) {
|
|
1373
1441
|
const sourcePos = Layout.anchorPos(g, seg, "source");
|
|
@@ -1413,7 +1481,11 @@ var Lines = class _Lines {
|
|
|
1413
1481
|
validTrack = track;
|
|
1414
1482
|
break;
|
|
1415
1483
|
}
|
|
1416
|
-
|
|
1484
|
+
const minA = Math.min(seg.p1, seg.p2);
|
|
1485
|
+
const maxA = Math.max(seg.p1, seg.p2);
|
|
1486
|
+
const minB = Math.min(other.p1, other.p2);
|
|
1487
|
+
const maxB = Math.max(other.p1, other.p2);
|
|
1488
|
+
if (minA < maxB && minB < maxA) {
|
|
1417
1489
|
overlap = true;
|
|
1418
1490
|
break;
|
|
1419
1491
|
}
|
|
@@ -1428,6 +1500,21 @@ var Lines = class _Lines {
|
|
|
1428
1500
|
else
|
|
1429
1501
|
trackSet.push([seg]);
|
|
1430
1502
|
}
|
|
1503
|
+
const midpoint = (s) => (s.p1 + s.p2) / 2;
|
|
1504
|
+
const sortTracks = (tracks2, goingRight) => {
|
|
1505
|
+
tracks2.sort((a, b) => {
|
|
1506
|
+
const midA = Math.max(...a.map(midpoint));
|
|
1507
|
+
const midB = Math.max(...b.map(midpoint));
|
|
1508
|
+
return goingRight ? midB - midA : midA - midB;
|
|
1509
|
+
});
|
|
1510
|
+
};
|
|
1511
|
+
sortTracks(rightTracks, true);
|
|
1512
|
+
sortTracks(leftTracks, false);
|
|
1513
|
+
allTracks.sort((a, b) => {
|
|
1514
|
+
const avgA = a.reduce((sum, s) => sum + midpoint(s), 0) / a.length;
|
|
1515
|
+
const avgB = b.reduce((sum, s) => sum + midpoint(s), 0) / b.length;
|
|
1516
|
+
return avgB - avgA;
|
|
1517
|
+
});
|
|
1431
1518
|
const tracks = [];
|
|
1432
1519
|
const all = leftTracks.concat(rightTracks).concat(allTracks);
|
|
1433
1520
|
for (const track of all)
|
|
@@ -1598,6 +1685,7 @@ var Lines = class _Lines {
|
|
|
1598
1685
|
};
|
|
1599
1686
|
|
|
1600
1687
|
// src/graph/graph.ts
|
|
1688
|
+
var log9 = logger("graph");
|
|
1601
1689
|
var emptyChanges = {
|
|
1602
1690
|
addedNodes: [],
|
|
1603
1691
|
removedNodes: [],
|
|
@@ -1872,29 +1960,8 @@ var screenPos = (x, y) => ({ x, y });
|
|
|
1872
1960
|
var canvasPos = (x, y) => ({ x, y });
|
|
1873
1961
|
var graphPos = (x, y) => ({ x, y });
|
|
1874
1962
|
|
|
1875
|
-
// src/canvas/node.tsx
|
|
1876
|
-
import styles from "./node.css?raw";
|
|
1877
|
-
|
|
1878
|
-
// src/canvas/styler.ts
|
|
1879
|
-
var injected = {};
|
|
1880
|
-
function styler(name, styles4, prefix) {
|
|
1881
|
-
if (prefix === "g3p" && !injected[name]) {
|
|
1882
|
-
const style = document.createElement("style");
|
|
1883
|
-
style.textContent = styles4;
|
|
1884
|
-
document.head.appendChild(style);
|
|
1885
|
-
injected[name] = true;
|
|
1886
|
-
}
|
|
1887
|
-
return (str, condition) => {
|
|
1888
|
-
if (!(condition ?? true)) return "";
|
|
1889
|
-
const parts = str.split(/\s+/);
|
|
1890
|
-
const fixed = parts.map((p) => `${prefix}-${name}-${p}`);
|
|
1891
|
-
return fixed.join(" ");
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
1963
|
// src/canvas/node.tsx
|
|
1896
1964
|
import { jsx as jsx2, jsxs } from "jsx-dom/jsx-runtime";
|
|
1897
|
-
var log8 = logger("canvas");
|
|
1898
1965
|
var Node2 = class {
|
|
1899
1966
|
selected;
|
|
1900
1967
|
hovered;
|
|
@@ -1902,7 +1969,6 @@ var Node2 = class {
|
|
|
1902
1969
|
content;
|
|
1903
1970
|
canvas;
|
|
1904
1971
|
data;
|
|
1905
|
-
classPrefix;
|
|
1906
1972
|
isDummy;
|
|
1907
1973
|
pos;
|
|
1908
1974
|
constructor(canvas, data, isDummy = false) {
|
|
@@ -1910,20 +1976,18 @@ var Node2 = class {
|
|
|
1910
1976
|
this.data = data;
|
|
1911
1977
|
this.selected = false;
|
|
1912
1978
|
this.hovered = false;
|
|
1913
|
-
this.classPrefix = canvas.classPrefix;
|
|
1914
1979
|
this.isDummy = isDummy;
|
|
1915
1980
|
if (this.isDummy) {
|
|
1916
1981
|
const size = canvas.dummyNodeSize;
|
|
1917
1982
|
} else {
|
|
1918
1983
|
const render = data.render ?? canvas.renderNode;
|
|
1919
|
-
this.content = this.renderContent(render(data.data));
|
|
1984
|
+
this.content = this.renderContent(render(data.data, data));
|
|
1920
1985
|
}
|
|
1921
1986
|
}
|
|
1922
1987
|
remove() {
|
|
1923
1988
|
this.container.remove();
|
|
1924
1989
|
}
|
|
1925
1990
|
append() {
|
|
1926
|
-
console.log("append", this);
|
|
1927
1991
|
this.canvas.group.appendChild(this.container);
|
|
1928
1992
|
}
|
|
1929
1993
|
needsContentSize() {
|
|
@@ -1932,19 +1996,6 @@ var Node2 = class {
|
|
|
1932
1996
|
needsContainerSize() {
|
|
1933
1997
|
return !this.isDummy;
|
|
1934
1998
|
}
|
|
1935
|
-
handleClick(e) {
|
|
1936
|
-
e.stopPropagation();
|
|
1937
|
-
}
|
|
1938
|
-
handleMouseEnter(e) {
|
|
1939
|
-
}
|
|
1940
|
-
handleMouseLeave(e) {
|
|
1941
|
-
}
|
|
1942
|
-
handleContextMenu(e) {
|
|
1943
|
-
}
|
|
1944
|
-
handleMouseDown(e) {
|
|
1945
|
-
}
|
|
1946
|
-
handleMouseUp(e) {
|
|
1947
|
-
}
|
|
1948
1999
|
setPos(pos) {
|
|
1949
2000
|
this.pos = pos;
|
|
1950
2001
|
const { x, y } = pos;
|
|
@@ -1961,7 +2012,6 @@ var Node2 = class {
|
|
|
1961
2012
|
return el;
|
|
1962
2013
|
}
|
|
1963
2014
|
renderContainer() {
|
|
1964
|
-
const c = styler("node", styles, this.classPrefix);
|
|
1965
2015
|
const hasPorts = this.hasPorts();
|
|
1966
2016
|
const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
|
|
1967
2017
|
const nodeType = this.data?.type;
|
|
@@ -1969,14 +2019,8 @@ var Node2 = class {
|
|
|
1969
2019
|
this.container = /* @__PURE__ */ jsx2(
|
|
1970
2020
|
"g",
|
|
1971
2021
|
{
|
|
1972
|
-
className:
|
|
1973
|
-
|
|
1974
|
-
onMouseEnter: (e) => this.handleMouseEnter(e),
|
|
1975
|
-
onMouseLeave: (e) => this.handleMouseLeave(e),
|
|
1976
|
-
onContextMenu: (e) => this.handleContextMenu(e),
|
|
1977
|
-
onMouseDown: (e) => this.handleMouseDown(e),
|
|
1978
|
-
onMouseUp: (e) => this.handleMouseUp(e),
|
|
1979
|
-
style: { cursor: "pointer" },
|
|
2022
|
+
className: `g3p-node-container ${this.isDummy ? "g3p-node-dummy" : ""} ${typeClass}`.trim(),
|
|
2023
|
+
"data-node-id": this.data?.id,
|
|
1980
2024
|
children: inner
|
|
1981
2025
|
}
|
|
1982
2026
|
);
|
|
@@ -1986,7 +2030,6 @@ var Node2 = class {
|
|
|
1986
2030
|
return /* @__PURE__ */ jsx2("foreignObject", { width: w, height: h, children: this.content });
|
|
1987
2031
|
}
|
|
1988
2032
|
renderDummy() {
|
|
1989
|
-
const c = styler("node", styles, this.classPrefix);
|
|
1990
2033
|
let w = this.canvas.dummyNodeSize;
|
|
1991
2034
|
let h = this.canvas.dummyNodeSize;
|
|
1992
2035
|
w /= 2;
|
|
@@ -1999,7 +2042,7 @@ var Node2 = class {
|
|
|
1999
2042
|
cy: h,
|
|
2000
2043
|
rx: w,
|
|
2001
2044
|
ry: h,
|
|
2002
|
-
className:
|
|
2045
|
+
className: "g3p-node-background"
|
|
2003
2046
|
}
|
|
2004
2047
|
),
|
|
2005
2048
|
/* @__PURE__ */ jsx2(
|
|
@@ -2010,7 +2053,7 @@ var Node2 = class {
|
|
|
2010
2053
|
rx: w,
|
|
2011
2054
|
ry: h,
|
|
2012
2055
|
fill: "none",
|
|
2013
|
-
className:
|
|
2056
|
+
className: "g3p-node-border"
|
|
2014
2057
|
}
|
|
2015
2058
|
)
|
|
2016
2059
|
] });
|
|
@@ -2023,7 +2066,7 @@ var Node2 = class {
|
|
|
2023
2066
|
const ports = data.ports?.[dir];
|
|
2024
2067
|
if (!ports) continue;
|
|
2025
2068
|
for (const port of ports) {
|
|
2026
|
-
const el = this.content.querySelector(
|
|
2069
|
+
const el = this.content.querySelector(`.g3p-node-port[data-node-id="${data.id}"][data-port-id="${port.id}"]`);
|
|
2027
2070
|
if (!el) continue;
|
|
2028
2071
|
const portRect = el.getBoundingClientRect();
|
|
2029
2072
|
if (isVertical) {
|
|
@@ -2061,23 +2104,22 @@ var Node2 = class {
|
|
|
2061
2104
|
renderPortRow(dir, inout) {
|
|
2062
2105
|
const ports = this.data?.ports?.[dir];
|
|
2063
2106
|
if (!ports?.length) return null;
|
|
2064
|
-
const c = styler("node", styles, this.classPrefix);
|
|
2065
2107
|
const pos = this.getPortPosition(dir);
|
|
2066
2108
|
const isVertical = this.isVerticalOrientation();
|
|
2067
2109
|
const layoutClass = isVertical ? "row" : "col";
|
|
2068
2110
|
const rotateLabels = false;
|
|
2069
2111
|
const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
|
|
2070
|
-
return /* @__PURE__ */ jsx2("div", { className:
|
|
2112
|
+
return /* @__PURE__ */ jsx2("div", { className: `g3p-node-ports g3p-node-ports-${layoutClass}`, children: ports.map((port) => /* @__PURE__ */ jsx2(
|
|
2071
2113
|
"div",
|
|
2072
2114
|
{
|
|
2073
|
-
|
|
2074
|
-
|
|
2115
|
+
className: `g3p-node-port g3p-node-port-${inout}-${pos} ${rotateClass}`,
|
|
2116
|
+
"data-node-id": this.data.id,
|
|
2117
|
+
"data-port-id": port.id,
|
|
2075
2118
|
children: port.label ?? port.id
|
|
2076
2119
|
}
|
|
2077
2120
|
)) });
|
|
2078
2121
|
}
|
|
2079
2122
|
renderInsidePorts(el) {
|
|
2080
|
-
const c = styler("node", styles, this.classPrefix);
|
|
2081
2123
|
const isVertical = this.isVerticalOrientation();
|
|
2082
2124
|
const isReversed = this.isReversedOrientation();
|
|
2083
2125
|
let inPorts = this.renderPortRow("in", "in");
|
|
@@ -2085,14 +2127,13 @@ var Node2 = class {
|
|
|
2085
2127
|
if (!inPorts && !outPorts) return el;
|
|
2086
2128
|
if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
|
|
2087
2129
|
const wrapperClass = isVertical ? "v" : "h";
|
|
2088
|
-
return /* @__PURE__ */ jsxs("div", { className:
|
|
2130
|
+
return /* @__PURE__ */ jsxs("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
|
|
2089
2131
|
inPorts,
|
|
2090
2132
|
el,
|
|
2091
2133
|
outPorts
|
|
2092
2134
|
] });
|
|
2093
2135
|
}
|
|
2094
2136
|
renderOutsidePorts(el) {
|
|
2095
|
-
const c = styler("node", styles, this.classPrefix);
|
|
2096
2137
|
const isVertical = this.isVerticalOrientation();
|
|
2097
2138
|
const isReversed = this.isReversedOrientation();
|
|
2098
2139
|
let inPorts = this.renderPortRow("in", "out");
|
|
@@ -2100,71 +2141,59 @@ var Node2 = class {
|
|
|
2100
2141
|
if (!inPorts && !outPorts) return el;
|
|
2101
2142
|
if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
|
|
2102
2143
|
const wrapperClass = isVertical ? "v" : "h";
|
|
2103
|
-
return /* @__PURE__ */ jsxs("div", { className:
|
|
2144
|
+
return /* @__PURE__ */ jsxs("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
|
|
2104
2145
|
inPorts,
|
|
2105
2146
|
el,
|
|
2106
2147
|
outPorts
|
|
2107
2148
|
] });
|
|
2108
2149
|
}
|
|
2109
2150
|
renderBorder(el) {
|
|
2110
|
-
|
|
2111
|
-
return /* @__PURE__ */ jsx2("div", { className: c("border"), children: el });
|
|
2151
|
+
return /* @__PURE__ */ jsx2("div", { className: "g3p-node-border", children: el });
|
|
2112
2152
|
}
|
|
2113
2153
|
};
|
|
2114
2154
|
|
|
2115
2155
|
// src/canvas/seg.tsx
|
|
2116
|
-
import styles2 from "./seg.css?raw";
|
|
2117
2156
|
import { jsx as jsx3, jsxs as jsxs2 } from "jsx-dom/jsx-runtime";
|
|
2118
|
-
var log9 = logger("canvas");
|
|
2119
2157
|
var Seg2 = class {
|
|
2120
2158
|
id;
|
|
2121
2159
|
selected;
|
|
2122
2160
|
hovered;
|
|
2123
2161
|
canvas;
|
|
2124
|
-
classPrefix;
|
|
2125
2162
|
type;
|
|
2126
2163
|
svg;
|
|
2127
2164
|
el;
|
|
2128
2165
|
source;
|
|
2129
2166
|
target;
|
|
2167
|
+
edgeIds;
|
|
2130
2168
|
constructor(canvas, data, g) {
|
|
2131
2169
|
this.id = data.id;
|
|
2132
2170
|
this.canvas = canvas;
|
|
2133
2171
|
this.selected = false;
|
|
2134
2172
|
this.hovered = false;
|
|
2135
2173
|
this.svg = data.svg;
|
|
2136
|
-
this.classPrefix = canvas.classPrefix;
|
|
2137
2174
|
this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
|
|
2138
2175
|
this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
|
|
2139
2176
|
this.type = data.type;
|
|
2177
|
+
this.edgeIds = data.edgeIds.toArray();
|
|
2140
2178
|
this.el = this.render();
|
|
2141
2179
|
}
|
|
2142
|
-
handleClick(e) {
|
|
2143
|
-
e.stopPropagation();
|
|
2144
|
-
}
|
|
2145
|
-
handleMouseEnter(e) {
|
|
2146
|
-
}
|
|
2147
|
-
handleMouseLeave(e) {
|
|
2148
|
-
}
|
|
2149
|
-
handleContextMenu(e) {
|
|
2150
|
-
}
|
|
2151
2180
|
append() {
|
|
2152
2181
|
this.canvas.group.appendChild(this.el);
|
|
2153
2182
|
}
|
|
2154
2183
|
remove() {
|
|
2155
2184
|
this.el.remove();
|
|
2156
2185
|
}
|
|
2157
|
-
update(data) {
|
|
2186
|
+
update(data, g) {
|
|
2158
2187
|
this.svg = data.svg;
|
|
2159
2188
|
this.type = data.type;
|
|
2160
|
-
this.source = data.source;
|
|
2161
|
-
this.target = data.target;
|
|
2189
|
+
this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
|
|
2190
|
+
this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
|
|
2191
|
+
this.edgeIds = data.edgeIds.toArray();
|
|
2162
2192
|
this.remove();
|
|
2163
2193
|
this.el = this.render();
|
|
2164
2194
|
this.append();
|
|
2165
2195
|
}
|
|
2166
2196
|
render() {
|
|
2167
|
-
const c = styler("seg", styles2, this.classPrefix);
|
|
2168
2197
|
let { source, target } = normalize(this);
|
|
2169
2198
|
if (this.source.isDummy) source = void 0;
|
|
2170
2199
|
if (this.target.isDummy) target = void 0;
|
|
@@ -2174,18 +2203,15 @@ var Seg2 = class {
|
|
|
2174
2203
|
{
|
|
2175
2204
|
ref: (el) => this.el = el,
|
|
2176
2205
|
id: `g3p-seg-${this.id}`,
|
|
2177
|
-
className:
|
|
2178
|
-
|
|
2179
|
-
onMouseEnter: this.handleMouseEnter.bind(this),
|
|
2180
|
-
onMouseLeave: this.handleMouseLeave.bind(this),
|
|
2181
|
-
onContextMenu: this.handleContextMenu.bind(this),
|
|
2206
|
+
className: `g3p-seg-container ${typeClass}`.trim(),
|
|
2207
|
+
"data-edge-id": this.id,
|
|
2182
2208
|
children: [
|
|
2183
2209
|
/* @__PURE__ */ jsx3(
|
|
2184
2210
|
"path",
|
|
2185
2211
|
{
|
|
2186
2212
|
d: this.svg,
|
|
2187
2213
|
fill: "none",
|
|
2188
|
-
className:
|
|
2214
|
+
className: "g3p-seg-line",
|
|
2189
2215
|
markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
|
|
2190
2216
|
markerEnd: target ? `url(#g3p-marker-${target})` : void 0
|
|
2191
2217
|
}
|
|
@@ -2196,7 +2222,7 @@ var Seg2 = class {
|
|
|
2196
2222
|
d: this.svg,
|
|
2197
2223
|
stroke: "transparent",
|
|
2198
2224
|
fill: "none",
|
|
2199
|
-
className:
|
|
2225
|
+
className: "g3p-seg-hitbox",
|
|
2200
2226
|
style: { cursor: "pointer" }
|
|
2201
2227
|
}
|
|
2202
2228
|
)
|
|
@@ -2206,46 +2232,561 @@ var Seg2 = class {
|
|
|
2206
2232
|
}
|
|
2207
2233
|
};
|
|
2208
2234
|
|
|
2209
|
-
// src/canvas/
|
|
2210
|
-
|
|
2211
|
-
|
|
2235
|
+
// src/canvas/editMode.ts
|
|
2236
|
+
var EditMode = class {
|
|
2237
|
+
_state = { type: "idle" };
|
|
2238
|
+
_editable = false;
|
|
2239
|
+
get state() {
|
|
2240
|
+
return this._state;
|
|
2241
|
+
}
|
|
2242
|
+
get editable() {
|
|
2243
|
+
return this._editable;
|
|
2244
|
+
}
|
|
2245
|
+
set editable(value) {
|
|
2246
|
+
this._editable = value;
|
|
2247
|
+
if (!value) {
|
|
2248
|
+
this.reset();
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
get isIdle() {
|
|
2252
|
+
return this._state.type === "idle";
|
|
2253
|
+
}
|
|
2254
|
+
get isPanning() {
|
|
2255
|
+
return this._state.type === "panning";
|
|
2256
|
+
}
|
|
2257
|
+
get isCreatingEdge() {
|
|
2258
|
+
return this._state.type === "new-edge";
|
|
2259
|
+
}
|
|
2260
|
+
/** Start panning the canvas */
|
|
2261
|
+
startPan(startCanvas, startTransform) {
|
|
2262
|
+
this._state = { type: "panning", startCanvas, startTransform };
|
|
2263
|
+
}
|
|
2264
|
+
/** Start creating a new edge from a node or port */
|
|
2265
|
+
startNewEdge(id, startGraph, port) {
|
|
2266
|
+
if (!this._editable) return;
|
|
2267
|
+
this._state = {
|
|
2268
|
+
type: "new-edge",
|
|
2269
|
+
source: { id, port },
|
|
2270
|
+
startGraph,
|
|
2271
|
+
currentGraph: startGraph,
|
|
2272
|
+
target: null
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
/** Update the current position during new-edge mode */
|
|
2276
|
+
updateNewEdgePosition(currentGraph) {
|
|
2277
|
+
if (this._state.type === "new-edge") {
|
|
2278
|
+
this._state = { ...this._state, currentGraph };
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/** Update the hover target during new-edge mode */
|
|
2282
|
+
setHoverTarget(target) {
|
|
2283
|
+
if (this._state.type === "new-edge") {
|
|
2284
|
+
this._state = { ...this._state, target };
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/** Get the new-edge state if active */
|
|
2288
|
+
getNewEdgeState() {
|
|
2289
|
+
if (this._state.type === "new-edge") {
|
|
2290
|
+
return this._state;
|
|
2291
|
+
}
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
/** Reset to idle state */
|
|
2295
|
+
reset() {
|
|
2296
|
+
this._state = { type: "idle" };
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
// src/canvas/newEdge.tsx
|
|
2212
2301
|
import { jsx as jsx4, jsxs as jsxs3 } from "jsx-dom/jsx-runtime";
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2302
|
+
function renderNewEdge({ start, end }) {
|
|
2303
|
+
return /* @__PURE__ */ jsxs3("g", { className: "g3p-new-edge-container", children: [
|
|
2304
|
+
/* @__PURE__ */ jsx4(
|
|
2305
|
+
"circle",
|
|
2306
|
+
{
|
|
2307
|
+
cx: start.x,
|
|
2308
|
+
cy: start.y,
|
|
2309
|
+
r: 4,
|
|
2310
|
+
className: "g3p-new-edge-origin"
|
|
2311
|
+
}
|
|
2312
|
+
),
|
|
2313
|
+
/* @__PURE__ */ jsx4(
|
|
2314
|
+
"line",
|
|
2315
|
+
{
|
|
2316
|
+
x1: start.x,
|
|
2317
|
+
y1: start.y,
|
|
2318
|
+
x2: end.x,
|
|
2319
|
+
y2: end.y,
|
|
2320
|
+
className: "g3p-new-edge-line"
|
|
2321
|
+
}
|
|
2322
|
+
),
|
|
2323
|
+
/* @__PURE__ */ jsx4(
|
|
2324
|
+
"circle",
|
|
2325
|
+
{
|
|
2326
|
+
cx: end.x,
|
|
2327
|
+
cy: end.y,
|
|
2328
|
+
r: 3,
|
|
2329
|
+
className: "g3p-new-edge-end"
|
|
2330
|
+
}
|
|
2331
|
+
)
|
|
2332
|
+
] });
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// src/canvas/modal.tsx
|
|
2336
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "jsx-dom/jsx-runtime";
|
|
2337
|
+
var Modal = class {
|
|
2338
|
+
container;
|
|
2339
|
+
overlay;
|
|
2340
|
+
dialog;
|
|
2341
|
+
onClose;
|
|
2342
|
+
mouseDownOnOverlay = false;
|
|
2343
|
+
constructor(options) {
|
|
2344
|
+
this.onClose = options.onClose;
|
|
2345
|
+
this.overlay = /* @__PURE__ */ jsx5(
|
|
2346
|
+
"div",
|
|
2347
|
+
{
|
|
2348
|
+
className: "g3p-modal-overlay",
|
|
2349
|
+
onMouseDown: (e) => {
|
|
2350
|
+
this.mouseDownOnOverlay = e.target === this.overlay;
|
|
2351
|
+
},
|
|
2352
|
+
onMouseUp: (e) => {
|
|
2353
|
+
if (this.mouseDownOnOverlay && e.target === this.overlay) this.close();
|
|
2354
|
+
this.mouseDownOnOverlay = false;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
);
|
|
2358
|
+
this.dialog = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-dialog", children: [
|
|
2359
|
+
/* @__PURE__ */ jsxs4("div", { className: "g3p-modal-header", children: [
|
|
2360
|
+
/* @__PURE__ */ jsx5("span", { className: "g3p-modal-title", children: options.title }),
|
|
2361
|
+
/* @__PURE__ */ jsx5(
|
|
2362
|
+
"button",
|
|
2363
|
+
{
|
|
2364
|
+
className: "g3p-modal-close",
|
|
2365
|
+
onClick: () => this.close(),
|
|
2366
|
+
children: "\xD7"
|
|
2367
|
+
}
|
|
2368
|
+
)
|
|
2369
|
+
] }),
|
|
2370
|
+
/* @__PURE__ */ jsx5("div", { className: "g3p-modal-body" }),
|
|
2371
|
+
/* @__PURE__ */ jsx5("div", { className: "g3p-modal-footer" })
|
|
2372
|
+
] });
|
|
2373
|
+
if (options.position) {
|
|
2374
|
+
this.dialog.style.position = "absolute";
|
|
2375
|
+
this.dialog.style.left = `${options.position.x}px`;
|
|
2376
|
+
this.dialog.style.top = `${options.position.y}px`;
|
|
2377
|
+
this.dialog.style.transform = "translate(-50%, -50%)";
|
|
2378
|
+
}
|
|
2379
|
+
this.overlay.appendChild(this.dialog);
|
|
2380
|
+
this.container = this.overlay;
|
|
2381
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
2382
|
+
document.addEventListener("keydown", this.handleKeyDown);
|
|
2383
|
+
}
|
|
2384
|
+
handleKeyDown(e) {
|
|
2385
|
+
if (e.key === "Escape") {
|
|
2386
|
+
this.close();
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
get body() {
|
|
2390
|
+
return this.dialog.querySelector(".g3p-modal-body");
|
|
2391
|
+
}
|
|
2392
|
+
get footer() {
|
|
2393
|
+
return this.dialog.querySelector(".g3p-modal-footer");
|
|
2394
|
+
}
|
|
2395
|
+
show(parent) {
|
|
2396
|
+
parent.appendChild(this.container);
|
|
2397
|
+
const firstInput = this.dialog.querySelector("input, select, button");
|
|
2398
|
+
if (firstInput) firstInput.focus();
|
|
2399
|
+
}
|
|
2400
|
+
close() {
|
|
2401
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2402
|
+
this.container.remove();
|
|
2403
|
+
this.onClose();
|
|
2404
|
+
}
|
|
2228
2405
|
};
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2406
|
+
var NewNodeModal = class extends Modal {
|
|
2407
|
+
fieldInputs = /* @__PURE__ */ new Map();
|
|
2408
|
+
typeSelect;
|
|
2409
|
+
submitCallback;
|
|
2410
|
+
fields;
|
|
2411
|
+
constructor(options) {
|
|
2412
|
+
super({
|
|
2413
|
+
title: "New Node",
|
|
2414
|
+
onClose: () => options.onCancel?.()
|
|
2415
|
+
});
|
|
2416
|
+
this.submitCallback = options.onSubmit;
|
|
2417
|
+
this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
|
|
2418
|
+
this.renderBody(options.nodeTypes);
|
|
2419
|
+
this.renderFooter();
|
|
2420
|
+
}
|
|
2421
|
+
renderBody(nodeTypes) {
|
|
2422
|
+
this.body.innerHTML = "";
|
|
2423
|
+
for (const [name, type] of this.fields) {
|
|
2424
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2425
|
+
const fieldGroup = this.renderField(name, label, type);
|
|
2426
|
+
this.body.appendChild(fieldGroup);
|
|
2240
2427
|
}
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
2243
|
-
|
|
2428
|
+
if (nodeTypes && nodeTypes.length > 0) {
|
|
2429
|
+
const typeGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2430
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Type" }),
|
|
2431
|
+
/* @__PURE__ */ jsxs4(
|
|
2432
|
+
"select",
|
|
2433
|
+
{
|
|
2434
|
+
className: "g3p-modal-select",
|
|
2435
|
+
ref: (el) => this.typeSelect = el,
|
|
2436
|
+
children: [
|
|
2437
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "Default" }),
|
|
2438
|
+
nodeTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, children: type }))
|
|
2439
|
+
]
|
|
2440
|
+
}
|
|
2441
|
+
)
|
|
2442
|
+
] });
|
|
2443
|
+
this.body.appendChild(typeGroup);
|
|
2244
2444
|
}
|
|
2245
2445
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2446
|
+
renderField(name, label, type) {
|
|
2447
|
+
if (type === "boolean") {
|
|
2448
|
+
return /* @__PURE__ */ jsx5("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ jsxs4("label", { className: "g3p-modal-label", children: [
|
|
2449
|
+
/* @__PURE__ */ jsx5(
|
|
2450
|
+
"input",
|
|
2451
|
+
{
|
|
2452
|
+
type: "checkbox",
|
|
2453
|
+
className: "g3p-modal-checkbox",
|
|
2454
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2455
|
+
}
|
|
2456
|
+
),
|
|
2457
|
+
label
|
|
2458
|
+
] }) });
|
|
2459
|
+
}
|
|
2460
|
+
return /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2461
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: label }),
|
|
2462
|
+
/* @__PURE__ */ jsx5(
|
|
2463
|
+
"input",
|
|
2464
|
+
{
|
|
2465
|
+
type: type === "number" ? "number" : "text",
|
|
2466
|
+
className: "g3p-modal-input",
|
|
2467
|
+
placeholder: `Enter ${label.toLowerCase()}`,
|
|
2468
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2469
|
+
}
|
|
2470
|
+
)
|
|
2471
|
+
] });
|
|
2472
|
+
}
|
|
2473
|
+
renderFooter() {
|
|
2474
|
+
this.footer.innerHTML = "";
|
|
2475
|
+
this.footer.appendChild(
|
|
2476
|
+
/* @__PURE__ */ jsxs4("div", { className: "g3p-modal-buttons", children: [
|
|
2477
|
+
/* @__PURE__ */ jsx5(
|
|
2478
|
+
"button",
|
|
2479
|
+
{
|
|
2480
|
+
className: "g3p-modal-btn g3p-modal-btn-secondary",
|
|
2481
|
+
onClick: () => this.close(),
|
|
2482
|
+
children: "Cancel"
|
|
2483
|
+
}
|
|
2484
|
+
),
|
|
2485
|
+
/* @__PURE__ */ jsx5(
|
|
2486
|
+
"button",
|
|
2487
|
+
{
|
|
2488
|
+
className: "g3p-modal-btn g3p-modal-btn-primary",
|
|
2489
|
+
onClick: () => this.submit(),
|
|
2490
|
+
children: "Create"
|
|
2491
|
+
}
|
|
2492
|
+
)
|
|
2493
|
+
] })
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
submit() {
|
|
2497
|
+
const data = {};
|
|
2498
|
+
for (const [name, type] of this.fields) {
|
|
2499
|
+
const input = this.fieldInputs.get(name);
|
|
2500
|
+
if (!input) continue;
|
|
2501
|
+
if (type === "boolean") {
|
|
2502
|
+
data[name] = input.checked;
|
|
2503
|
+
} else if (type === "number") {
|
|
2504
|
+
const val = input.value;
|
|
2505
|
+
if (val) data[name] = Number(val);
|
|
2506
|
+
} else {
|
|
2507
|
+
const val = input.value.trim();
|
|
2508
|
+
if (val) data[name] = val;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
if (Object.keys(data).length === 0) {
|
|
2512
|
+
const firstInput = this.fieldInputs.values().next().value;
|
|
2513
|
+
if (firstInput) firstInput.focus();
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
if (this.typeSelect?.value) {
|
|
2517
|
+
data.type = this.typeSelect.value;
|
|
2518
|
+
}
|
|
2519
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2520
|
+
this.container.remove();
|
|
2521
|
+
this.submitCallback(data);
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
var EditNodeModal = class extends Modal {
|
|
2525
|
+
fieldInputs = /* @__PURE__ */ new Map();
|
|
2526
|
+
typeSelect;
|
|
2527
|
+
node;
|
|
2528
|
+
fields;
|
|
2529
|
+
submitCallback;
|
|
2530
|
+
deleteCallback;
|
|
2531
|
+
constructor(options) {
|
|
2532
|
+
super({
|
|
2533
|
+
title: "Edit Node",
|
|
2534
|
+
position: options.position,
|
|
2535
|
+
onClose: () => options.onCancel?.()
|
|
2536
|
+
});
|
|
2537
|
+
this.node = options.node;
|
|
2538
|
+
this.submitCallback = options.onSubmit;
|
|
2539
|
+
this.deleteCallback = options.onDelete;
|
|
2540
|
+
this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
|
|
2541
|
+
if (!options.fields && !this.node.title)
|
|
2542
|
+
this.node = { ...this.node, title: this.node.id };
|
|
2543
|
+
this.renderBody(options.nodeTypes);
|
|
2544
|
+
this.renderFooter();
|
|
2545
|
+
}
|
|
2546
|
+
renderBody(nodeTypes) {
|
|
2547
|
+
console.log(`renderBody`, this.node);
|
|
2548
|
+
this.body.innerHTML = "";
|
|
2549
|
+
for (const [name, type] of this.fields) {
|
|
2550
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2551
|
+
const currentValue = this.node[name];
|
|
2552
|
+
const fieldGroup = this.renderField(name, label, type, currentValue);
|
|
2553
|
+
this.body.appendChild(fieldGroup);
|
|
2554
|
+
}
|
|
2555
|
+
if (nodeTypes && nodeTypes.length > 0) {
|
|
2556
|
+
const currentType = this.node.type ?? "";
|
|
2557
|
+
const typeGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2558
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Type" }),
|
|
2559
|
+
/* @__PURE__ */ jsxs4(
|
|
2560
|
+
"select",
|
|
2561
|
+
{
|
|
2562
|
+
className: "g3p-modal-select",
|
|
2563
|
+
ref: (el) => this.typeSelect = el,
|
|
2564
|
+
children: [
|
|
2565
|
+
/* @__PURE__ */ jsx5("option", { value: "", selected: !currentType, children: "Default" }),
|
|
2566
|
+
nodeTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentType, children: type }))
|
|
2567
|
+
]
|
|
2568
|
+
}
|
|
2569
|
+
)
|
|
2570
|
+
] });
|
|
2571
|
+
this.body.appendChild(typeGroup);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
renderField(name, label, type, currentValue) {
|
|
2575
|
+
if (type === "boolean") {
|
|
2576
|
+
return /* @__PURE__ */ jsx5("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ jsxs4("label", { className: "g3p-modal-label", children: [
|
|
2577
|
+
/* @__PURE__ */ jsx5(
|
|
2578
|
+
"input",
|
|
2579
|
+
{
|
|
2580
|
+
type: "checkbox",
|
|
2581
|
+
className: "g3p-modal-checkbox",
|
|
2582
|
+
checked: !!currentValue,
|
|
2583
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2584
|
+
}
|
|
2585
|
+
),
|
|
2586
|
+
label
|
|
2587
|
+
] }) });
|
|
2588
|
+
}
|
|
2589
|
+
return /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2590
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: label }),
|
|
2591
|
+
/* @__PURE__ */ jsx5(
|
|
2592
|
+
"input",
|
|
2593
|
+
{
|
|
2594
|
+
type: type === "number" ? "number" : "text",
|
|
2595
|
+
className: "g3p-modal-input",
|
|
2596
|
+
value: currentValue ?? "",
|
|
2597
|
+
ref: (el) => this.fieldInputs.set(name, el)
|
|
2598
|
+
}
|
|
2599
|
+
)
|
|
2600
|
+
] });
|
|
2601
|
+
}
|
|
2602
|
+
renderFooter() {
|
|
2603
|
+
this.footer.innerHTML = "";
|
|
2604
|
+
this.footer.appendChild(
|
|
2605
|
+
/* @__PURE__ */ jsxs4("div", { className: "g3p-modal-buttons", children: [
|
|
2606
|
+
this.deleteCallback && /* @__PURE__ */ jsx5(
|
|
2607
|
+
"button",
|
|
2608
|
+
{
|
|
2609
|
+
className: "g3p-modal-btn g3p-modal-btn-danger",
|
|
2610
|
+
onClick: () => this.delete(),
|
|
2611
|
+
children: "Delete"
|
|
2612
|
+
}
|
|
2613
|
+
),
|
|
2614
|
+
/* @__PURE__ */ jsx5("div", { className: "g3p-modal-spacer" }),
|
|
2615
|
+
/* @__PURE__ */ jsx5(
|
|
2616
|
+
"button",
|
|
2617
|
+
{
|
|
2618
|
+
className: "g3p-modal-btn g3p-modal-btn-secondary",
|
|
2619
|
+
onClick: () => this.close(),
|
|
2620
|
+
children: "Cancel"
|
|
2621
|
+
}
|
|
2622
|
+
),
|
|
2623
|
+
/* @__PURE__ */ jsx5(
|
|
2624
|
+
"button",
|
|
2625
|
+
{
|
|
2626
|
+
className: "g3p-modal-btn g3p-modal-btn-primary",
|
|
2627
|
+
onClick: () => this.submit(),
|
|
2628
|
+
children: "Save"
|
|
2629
|
+
}
|
|
2630
|
+
)
|
|
2631
|
+
] })
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
submit() {
|
|
2635
|
+
const data = { ...this.node };
|
|
2636
|
+
for (const [name, type] of this.fields) {
|
|
2637
|
+
const input = this.fieldInputs.get(name);
|
|
2638
|
+
if (!input) continue;
|
|
2639
|
+
if (type === "boolean") {
|
|
2640
|
+
data[name] = input.checked;
|
|
2641
|
+
} else if (type === "number") {
|
|
2642
|
+
const val = input.value;
|
|
2643
|
+
data[name] = val ? Number(val) : void 0;
|
|
2644
|
+
} else {
|
|
2645
|
+
const val = input.value.trim();
|
|
2646
|
+
data[name] = val || void 0;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
if (this.typeSelect) {
|
|
2650
|
+
data.type = this.typeSelect.value || void 0;
|
|
2651
|
+
}
|
|
2652
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2653
|
+
this.container.remove();
|
|
2654
|
+
this.submitCallback(data);
|
|
2655
|
+
}
|
|
2656
|
+
delete() {
|
|
2657
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2658
|
+
this.container.remove();
|
|
2659
|
+
this.deleteCallback?.();
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
var EditEdgeModal = class _EditEdgeModal extends Modal {
|
|
2663
|
+
sourceMarkerSelect;
|
|
2664
|
+
targetMarkerSelect;
|
|
2665
|
+
typeSelect;
|
|
2666
|
+
edge;
|
|
2667
|
+
submitCallback;
|
|
2668
|
+
deleteCallback;
|
|
2669
|
+
static markerTypes = ["none", "arrow", "circle", "diamond", "bar"];
|
|
2670
|
+
constructor(options) {
|
|
2671
|
+
super({
|
|
2672
|
+
title: "Edit Edge",
|
|
2673
|
+
onClose: () => options.onCancel?.()
|
|
2674
|
+
});
|
|
2675
|
+
this.edge = options.edge;
|
|
2676
|
+
this.submitCallback = options.onSubmit;
|
|
2677
|
+
this.deleteCallback = options.onDelete;
|
|
2678
|
+
this.renderBody(options.edgeTypes);
|
|
2679
|
+
this.renderFooter();
|
|
2680
|
+
}
|
|
2681
|
+
renderBody(edgeTypes) {
|
|
2682
|
+
this.body.innerHTML = "";
|
|
2683
|
+
const currentSourceMarker = this.edge.source?.marker ?? "none";
|
|
2684
|
+
const currentTargetMarker = this.edge.target?.marker ?? "arrow";
|
|
2685
|
+
const sourceGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2686
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Source Marker" }),
|
|
2687
|
+
/* @__PURE__ */ jsx5(
|
|
2688
|
+
"select",
|
|
2689
|
+
{
|
|
2690
|
+
className: "g3p-modal-select",
|
|
2691
|
+
ref: (el) => this.sourceMarkerSelect = el,
|
|
2692
|
+
children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentSourceMarker, children: type }))
|
|
2693
|
+
}
|
|
2694
|
+
)
|
|
2695
|
+
] });
|
|
2696
|
+
this.body.appendChild(sourceGroup);
|
|
2697
|
+
const targetGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2698
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Target Marker" }),
|
|
2699
|
+
/* @__PURE__ */ jsx5(
|
|
2700
|
+
"select",
|
|
2701
|
+
{
|
|
2702
|
+
className: "g3p-modal-select",
|
|
2703
|
+
ref: (el) => this.targetMarkerSelect = el,
|
|
2704
|
+
children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentTargetMarker, children: type }))
|
|
2705
|
+
}
|
|
2706
|
+
)
|
|
2707
|
+
] });
|
|
2708
|
+
this.body.appendChild(targetGroup);
|
|
2709
|
+
if (edgeTypes && edgeTypes.length > 0) {
|
|
2710
|
+
const currentType = this.edge.type ?? "";
|
|
2711
|
+
const typeGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
|
|
2712
|
+
/* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Type" }),
|
|
2713
|
+
/* @__PURE__ */ jsxs4(
|
|
2714
|
+
"select",
|
|
2715
|
+
{
|
|
2716
|
+
className: "g3p-modal-select",
|
|
2717
|
+
ref: (el) => this.typeSelect = el,
|
|
2718
|
+
children: [
|
|
2719
|
+
/* @__PURE__ */ jsx5("option", { value: "", selected: !currentType, children: "Default" }),
|
|
2720
|
+
edgeTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentType, children: type }))
|
|
2721
|
+
]
|
|
2722
|
+
}
|
|
2723
|
+
)
|
|
2724
|
+
] });
|
|
2725
|
+
this.body.appendChild(typeGroup);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
renderFooter() {
|
|
2729
|
+
this.footer.innerHTML = "";
|
|
2730
|
+
this.footer.appendChild(
|
|
2731
|
+
/* @__PURE__ */ jsxs4("div", { className: "g3p-modal-buttons", children: [
|
|
2732
|
+
this.deleteCallback && /* @__PURE__ */ jsx5(
|
|
2733
|
+
"button",
|
|
2734
|
+
{
|
|
2735
|
+
className: "g3p-modal-btn g3p-modal-btn-danger",
|
|
2736
|
+
onClick: () => this.delete(),
|
|
2737
|
+
children: "Delete"
|
|
2738
|
+
}
|
|
2739
|
+
),
|
|
2740
|
+
/* @__PURE__ */ jsx5("div", { className: "g3p-modal-spacer" }),
|
|
2741
|
+
/* @__PURE__ */ jsx5(
|
|
2742
|
+
"button",
|
|
2743
|
+
{
|
|
2744
|
+
className: "g3p-modal-btn g3p-modal-btn-secondary",
|
|
2745
|
+
onClick: () => this.close(),
|
|
2746
|
+
children: "Cancel"
|
|
2747
|
+
}
|
|
2748
|
+
),
|
|
2749
|
+
/* @__PURE__ */ jsx5(
|
|
2750
|
+
"button",
|
|
2751
|
+
{
|
|
2752
|
+
className: "g3p-modal-btn g3p-modal-btn-primary",
|
|
2753
|
+
onClick: () => this.submit(),
|
|
2754
|
+
children: "Save"
|
|
2755
|
+
}
|
|
2756
|
+
)
|
|
2757
|
+
] })
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
submit() {
|
|
2761
|
+
const data = {
|
|
2762
|
+
...this.edge,
|
|
2763
|
+
source: {
|
|
2764
|
+
...this.edge.source,
|
|
2765
|
+
marker: this.sourceMarkerSelect.value === "none" ? void 0 : this.sourceMarkerSelect.value
|
|
2766
|
+
},
|
|
2767
|
+
target: {
|
|
2768
|
+
...this.edge.target,
|
|
2769
|
+
marker: this.targetMarkerSelect.value === "none" ? void 0 : this.targetMarkerSelect.value
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
if (this.typeSelect) {
|
|
2773
|
+
data.type = this.typeSelect.value || void 0;
|
|
2774
|
+
}
|
|
2775
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2776
|
+
this.container.remove();
|
|
2777
|
+
this.submitCallback(data);
|
|
2778
|
+
}
|
|
2779
|
+
delete() {
|
|
2780
|
+
document.removeEventListener("keydown", this.handleKeyDown);
|
|
2781
|
+
this.container.remove();
|
|
2782
|
+
this.deleteCallback?.();
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
|
|
2786
|
+
// src/canvas/canvas.tsx
|
|
2787
|
+
import styles from "./styles.css?raw";
|
|
2788
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "jsx-dom/jsx-runtime";
|
|
2789
|
+
var log10 = logger("canvas");
|
|
2249
2790
|
var Canvas = class {
|
|
2250
2791
|
container;
|
|
2251
2792
|
root;
|
|
@@ -2258,19 +2799,26 @@ var Canvas = class {
|
|
|
2258
2799
|
curSegs;
|
|
2259
2800
|
updating;
|
|
2260
2801
|
// Pan-zoom state
|
|
2261
|
-
isPanning = false;
|
|
2262
|
-
panStart = null;
|
|
2263
|
-
transformStart = null;
|
|
2264
2802
|
panScale = null;
|
|
2265
2803
|
zoomControls;
|
|
2266
|
-
|
|
2804
|
+
// Edit mode state machine
|
|
2805
|
+
editMode;
|
|
2806
|
+
api;
|
|
2807
|
+
// New-edge visual element
|
|
2808
|
+
newEdgeEl;
|
|
2809
|
+
// Pending drag state (for double-click debounce)
|
|
2810
|
+
pendingDrag = null;
|
|
2811
|
+
constructor(api, options) {
|
|
2267
2812
|
Object.assign(this, options);
|
|
2813
|
+
this.api = api;
|
|
2268
2814
|
this.allNodes = /* @__PURE__ */ new Map();
|
|
2269
2815
|
this.curNodes = /* @__PURE__ */ new Map();
|
|
2270
2816
|
this.curSegs = /* @__PURE__ */ new Map();
|
|
2271
2817
|
this.updating = false;
|
|
2272
2818
|
this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
|
|
2273
2819
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
2820
|
+
this.editMode = new EditMode();
|
|
2821
|
+
this.editMode.editable = this.editable;
|
|
2274
2822
|
this.createMeasurementContainer();
|
|
2275
2823
|
this.createCanvasContainer();
|
|
2276
2824
|
if (this.panZoom) this.setupPanZoom();
|
|
@@ -2315,7 +2863,6 @@ var Canvas = class {
|
|
|
2315
2863
|
if (gnode.isDummy) {
|
|
2316
2864
|
node = new Node2(this, gnode, true);
|
|
2317
2865
|
node.renderContainer();
|
|
2318
|
-
node.setPos(gnode.pos);
|
|
2319
2866
|
this.allNodes.set(key, node);
|
|
2320
2867
|
} else {
|
|
2321
2868
|
if (!this.allNodes.has(key))
|
|
@@ -2324,6 +2871,7 @@ var Canvas = class {
|
|
|
2324
2871
|
}
|
|
2325
2872
|
this.curNodes.set(gnode.id, node);
|
|
2326
2873
|
node.append();
|
|
2874
|
+
node.setPos(gnode.pos);
|
|
2327
2875
|
}
|
|
2328
2876
|
updateNode(gnode) {
|
|
2329
2877
|
if (gnode.isDummy) throw new Error("dummy node cannot be updated");
|
|
@@ -2345,10 +2893,10 @@ var Canvas = class {
|
|
|
2345
2893
|
this.curSegs.set(gseg.id, seg);
|
|
2346
2894
|
seg.append();
|
|
2347
2895
|
}
|
|
2348
|
-
updateSeg(gseg) {
|
|
2896
|
+
updateSeg(gseg, g) {
|
|
2349
2897
|
const seg = this.curSegs.get(gseg.id);
|
|
2350
2898
|
if (!seg) throw new Error("seg not found");
|
|
2351
|
-
seg.update(gseg);
|
|
2899
|
+
seg.update(gseg, g);
|
|
2352
2900
|
}
|
|
2353
2901
|
deleteSeg(gseg) {
|
|
2354
2902
|
const seg = this.curSegs.get(gseg.id);
|
|
@@ -2368,25 +2916,89 @@ var Canvas = class {
|
|
|
2368
2916
|
for (const node of newNodes.values()) {
|
|
2369
2917
|
node.measure(isVertical);
|
|
2370
2918
|
const { id, version } = node.data;
|
|
2371
|
-
const key =
|
|
2919
|
+
const key = `k:${id}:${version}`;
|
|
2372
2920
|
this.allNodes.set(key, node);
|
|
2373
2921
|
node.renderContainer();
|
|
2374
2922
|
}
|
|
2375
2923
|
this.measurement.innerHTML = "";
|
|
2376
2924
|
return newNodes;
|
|
2377
2925
|
}
|
|
2926
|
+
// ========== Mouse event handlers ==========
|
|
2378
2927
|
onClick(e) {
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2928
|
+
const hit = this.hitTest(e.clientX, e.clientY);
|
|
2929
|
+
if (hit.type === "node") {
|
|
2930
|
+
this.api.handleClickNode(hit.node.data.id);
|
|
2931
|
+
} else if (hit.type === "edge") {
|
|
2932
|
+
this.api.handleClickEdge(hit.segId);
|
|
2933
|
+
}
|
|
2383
2934
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2935
|
+
onDoubleClick(e) {
|
|
2936
|
+
if (this.pendingDrag) {
|
|
2937
|
+
window.clearTimeout(this.pendingDrag.timeout);
|
|
2938
|
+
this.pendingDrag = null;
|
|
2939
|
+
}
|
|
2940
|
+
if (!this.editMode.editable) return;
|
|
2941
|
+
const hit = this.hitTest(e.clientX, e.clientY);
|
|
2942
|
+
if (hit.type === "node") {
|
|
2943
|
+
if (hit.node.isDummy) return;
|
|
2944
|
+
this.api.handleEditNode(hit.node.data.id);
|
|
2945
|
+
} else if (hit.type === "edge") {
|
|
2946
|
+
this.api.handleEditEdge(hit.segId);
|
|
2947
|
+
} else {
|
|
2948
|
+
this.api.handleNewNode();
|
|
2949
|
+
}
|
|
2386
2950
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2951
|
+
// ========== Built-in Modals ==========
|
|
2952
|
+
/** Show the new node modal */
|
|
2953
|
+
showNewNodeModal(callback) {
|
|
2954
|
+
const nodeTypes = Object.keys(this.nodeTypes);
|
|
2955
|
+
const fields = this.api.getNodeFields();
|
|
2956
|
+
const modal = new NewNodeModal({
|
|
2957
|
+
nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
|
|
2958
|
+
fields: fields.size > 0 ? fields : void 0,
|
|
2959
|
+
onSubmit: (data) => {
|
|
2960
|
+
callback(data);
|
|
2961
|
+
}
|
|
2962
|
+
});
|
|
2963
|
+
modal.show(document.body);
|
|
2964
|
+
}
|
|
2965
|
+
/** Show the edit node modal */
|
|
2966
|
+
showEditNodeModal(node, callback) {
|
|
2967
|
+
const nodeTypes = Object.keys(this.nodeTypes);
|
|
2968
|
+
const fields = this.api.getNodeFields();
|
|
2969
|
+
const modal = new EditNodeModal({
|
|
2970
|
+
node,
|
|
2971
|
+
nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
|
|
2972
|
+
fields: fields.size > 0 ? fields : void 0,
|
|
2973
|
+
onSubmit: (data) => {
|
|
2974
|
+
callback(data);
|
|
2975
|
+
},
|
|
2976
|
+
onDelete: () => {
|
|
2977
|
+
this.api.handleDeleteNode(node.id);
|
|
2978
|
+
}
|
|
2979
|
+
});
|
|
2980
|
+
modal.show(document.body);
|
|
2981
|
+
}
|
|
2982
|
+
/** Show the edit edge modal */
|
|
2983
|
+
showEditEdgeModal(edge, callback) {
|
|
2984
|
+
const modal = new EditEdgeModal({
|
|
2985
|
+
edge,
|
|
2986
|
+
edgeTypes: Object.keys(this.edgeTypes),
|
|
2987
|
+
onSubmit: callback,
|
|
2988
|
+
onDelete: () => {
|
|
2989
|
+
this.api.handleDeleteEdge(edge.id);
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
modal.show(document.body);
|
|
2993
|
+
}
|
|
2994
|
+
onContextMenu(e) {
|
|
2995
|
+
}
|
|
2996
|
+
groupTransform() {
|
|
2997
|
+
return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
|
|
2998
|
+
}
|
|
2999
|
+
viewBox() {
|
|
3000
|
+
const p = this.padding;
|
|
3001
|
+
const x = this.bounds.min.x - p;
|
|
2390
3002
|
const y = this.bounds.min.y - p;
|
|
2391
3003
|
const w = this.bounds.max.x - this.bounds.min.x + p * 2;
|
|
2392
3004
|
const h = this.bounds.max.y - this.bounds.min.y + p * 2;
|
|
@@ -2394,53 +3006,52 @@ var Canvas = class {
|
|
|
2394
3006
|
}
|
|
2395
3007
|
generateDynamicStyles() {
|
|
2396
3008
|
let css = "";
|
|
2397
|
-
|
|
2398
|
-
css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
|
|
3009
|
+
css += themeToCSS(this.theme, `.g3p-canvas-container`);
|
|
2399
3010
|
for (const [type, vars] of Object.entries(this.nodeTypes)) {
|
|
2400
|
-
css += themeToCSS(vars,
|
|
3011
|
+
css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
|
|
2401
3012
|
}
|
|
2402
3013
|
for (const [type, vars] of Object.entries(this.edgeTypes)) {
|
|
2403
|
-
css += themeToCSS(vars,
|
|
3014
|
+
css += themeToCSS(vars, `.g3p-edge-type-${type}`);
|
|
2404
3015
|
}
|
|
2405
3016
|
return css;
|
|
2406
3017
|
}
|
|
2407
3018
|
createCanvasContainer() {
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
3019
|
+
if (!document.getElementById("g3p-styles")) {
|
|
3020
|
+
const baseStyleEl = document.createElement("style");
|
|
3021
|
+
baseStyleEl.id = "g3p-styles";
|
|
3022
|
+
baseStyleEl.textContent = styles;
|
|
3023
|
+
document.head.appendChild(baseStyleEl);
|
|
3024
|
+
}
|
|
2414
3025
|
const dynamicStyles = this.generateDynamicStyles();
|
|
2415
3026
|
if (dynamicStyles) {
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2418
|
-
document.head.appendChild(
|
|
3027
|
+
const dynamicStyleEl = document.createElement("style");
|
|
3028
|
+
dynamicStyleEl.textContent = dynamicStyles;
|
|
3029
|
+
document.head.appendChild(dynamicStyleEl);
|
|
2419
3030
|
}
|
|
2420
|
-
const c = styler("canvas", styles3, this.classPrefix);
|
|
2421
3031
|
const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
|
|
2422
|
-
this.container = /* @__PURE__ */
|
|
3032
|
+
this.container = /* @__PURE__ */ jsx6(
|
|
2423
3033
|
"div",
|
|
2424
3034
|
{
|
|
2425
|
-
className:
|
|
3035
|
+
className: `g3p-canvas-container ${colorModeClass}`.trim(),
|
|
2426
3036
|
ref: (el) => this.container = el,
|
|
2427
3037
|
onContextMenu: this.onContextMenu.bind(this),
|
|
2428
|
-
children: /* @__PURE__ */
|
|
3038
|
+
children: /* @__PURE__ */ jsxs5(
|
|
2429
3039
|
"svg",
|
|
2430
3040
|
{
|
|
2431
3041
|
ref: (el) => this.root = el,
|
|
2432
|
-
className:
|
|
3042
|
+
className: "g3p-canvas-root",
|
|
2433
3043
|
width: this.width,
|
|
2434
3044
|
height: this.height,
|
|
2435
3045
|
viewBox: this.viewBox(),
|
|
2436
3046
|
preserveAspectRatio: "xMidYMid meet",
|
|
2437
3047
|
onClick: this.onClick.bind(this),
|
|
3048
|
+
onDblClick: this.onDoubleClick.bind(this),
|
|
2438
3049
|
children: [
|
|
2439
|
-
/* @__PURE__ */
|
|
2440
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize,
|
|
2441
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize,
|
|
3050
|
+
/* @__PURE__ */ jsxs5("defs", { children: [
|
|
3051
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
|
|
3052
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
|
|
2442
3053
|
] }),
|
|
2443
|
-
/* @__PURE__ */
|
|
3054
|
+
/* @__PURE__ */ jsx6(
|
|
2444
3055
|
"g",
|
|
2445
3056
|
{
|
|
2446
3057
|
ref: (el) => this.group = el,
|
|
@@ -2459,14 +3070,34 @@ var Canvas = class {
|
|
|
2459
3070
|
this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
|
|
2460
3071
|
document.addEventListener("mousemove", this.onMouseMove.bind(this));
|
|
2461
3072
|
document.addEventListener("mouseup", this.onMouseUp.bind(this));
|
|
3073
|
+
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
|
2462
3074
|
this.createZoomControls();
|
|
2463
3075
|
}
|
|
3076
|
+
onKeyDown(e) {
|
|
3077
|
+
if (e.key === "Escape" && this.editMode.isCreatingEdge) {
|
|
3078
|
+
this.endNewEdge(true);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
2464
3081
|
/** Convert screen coordinates to canvas-relative coordinates */
|
|
2465
3082
|
screenToCanvas(screen) {
|
|
2466
3083
|
const rect = this.container.getBoundingClientRect();
|
|
2467
3084
|
return canvasPos(screen.x - rect.left, screen.y - rect.top);
|
|
2468
3085
|
}
|
|
2469
|
-
/**
|
|
3086
|
+
/** Convert canvas coordinates to graph coordinates */
|
|
3087
|
+
canvasToGraph(canvas) {
|
|
3088
|
+
const vb = this.currentViewBox();
|
|
3089
|
+
const { scale, offsetX, offsetY } = this.getEffectiveScale();
|
|
3090
|
+
return graphPos(
|
|
3091
|
+
vb.x - offsetX + canvas.x * scale,
|
|
3092
|
+
vb.y - offsetY + canvas.y * scale
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
/** Convert screen coordinates to graph coordinates */
|
|
3096
|
+
screenToGraph(screen) {
|
|
3097
|
+
const canvas = this.screenToCanvas(screen);
|
|
3098
|
+
return this.canvasToGraph(canvas);
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
2470
3101
|
* Get the effective scale from canvas pixels to graph units,
|
|
2471
3102
|
* accounting for preserveAspectRatio="xMidYMid meet" which uses
|
|
2472
3103
|
* the smaller scale (to fit) and centers the content.
|
|
@@ -2483,15 +3114,6 @@ var Canvas = class {
|
|
|
2483
3114
|
const offsetY = (actualH - vb.h) / 2;
|
|
2484
3115
|
return { scale, offsetX, offsetY };
|
|
2485
3116
|
}
|
|
2486
|
-
/** Convert canvas coordinates to graph coordinates */
|
|
2487
|
-
canvasToGraph(canvas) {
|
|
2488
|
-
const vb = this.currentViewBox();
|
|
2489
|
-
const { scale, offsetX, offsetY } = this.getEffectiveScale();
|
|
2490
|
-
return graphPos(
|
|
2491
|
-
vb.x - offsetX + canvas.x * scale,
|
|
2492
|
-
vb.y - offsetY + canvas.y * scale
|
|
2493
|
-
);
|
|
2494
|
-
}
|
|
2495
3117
|
/** Get current viewBox as an object */
|
|
2496
3118
|
currentViewBox() {
|
|
2497
3119
|
const p = this.padding;
|
|
@@ -2526,28 +3148,67 @@ var Canvas = class {
|
|
|
2526
3148
|
onMouseDown(e) {
|
|
2527
3149
|
if (e.button !== 0) return;
|
|
2528
3150
|
if (e.target.closest(".g3p-zoom-controls")) return;
|
|
2529
|
-
this.
|
|
2530
|
-
this.
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
3151
|
+
const hit = this.hitTest(e.clientX, e.clientY);
|
|
3152
|
+
if (this.editMode.editable && (hit.type === "node" || hit.type === "port")) {
|
|
3153
|
+
const node = hit.node;
|
|
3154
|
+
if (node.isDummy) return;
|
|
3155
|
+
e.preventDefault();
|
|
3156
|
+
e.stopPropagation();
|
|
3157
|
+
const startGraph = this.screenToGraph(hit.center);
|
|
3158
|
+
const portId = hit.type === "port" ? hit.port : void 0;
|
|
3159
|
+
this.pendingDrag = {
|
|
3160
|
+
timeout: window.setTimeout(() => {
|
|
3161
|
+
if (this.pendingDrag) {
|
|
3162
|
+
this.startNewEdge(this.pendingDrag.nodeId, this.pendingDrag.startGraph, this.pendingDrag.portId);
|
|
3163
|
+
this.pendingDrag = null;
|
|
3164
|
+
}
|
|
3165
|
+
}, 200),
|
|
3166
|
+
nodeId: node.data.id,
|
|
3167
|
+
startGraph,
|
|
3168
|
+
portId
|
|
3169
|
+
};
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
if (hit.type === "canvas" || hit.type === "edge") {
|
|
3173
|
+
const startCanvas = this.screenToCanvas(screenPos(e.clientX, e.clientY));
|
|
3174
|
+
this.editMode.startPan(startCanvas, { ...this.transform });
|
|
3175
|
+
const { scale } = this.getEffectiveScale();
|
|
3176
|
+
this.panScale = { x: scale, y: scale };
|
|
3177
|
+
this.container.style.cursor = "grabbing";
|
|
3178
|
+
e.preventDefault();
|
|
3179
|
+
}
|
|
2536
3180
|
}
|
|
2537
3181
|
onMouseMove(e) {
|
|
2538
|
-
if (
|
|
3182
|
+
if (this.editMode.isCreatingEdge) {
|
|
3183
|
+
const screenCursor = screenPos(e.clientX, e.clientY);
|
|
3184
|
+
const canvasCursor = this.screenToCanvas(screenCursor);
|
|
3185
|
+
const graphCursor = this.canvasToGraph(canvasCursor);
|
|
3186
|
+
this.editMode.updateNewEdgePosition(graphCursor);
|
|
3187
|
+
this.updateNewEdgeVisual();
|
|
3188
|
+
this.detectHoverTarget(e.clientX, e.clientY);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
if (!this.editMode.isPanning || !this.panScale) return;
|
|
3192
|
+
const panState = this.editMode.state;
|
|
3193
|
+
if (panState.type !== "panning") return;
|
|
2539
3194
|
const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
|
|
2540
|
-
const dx = current.x -
|
|
2541
|
-
const dy = current.y -
|
|
2542
|
-
this.transform.x =
|
|
2543
|
-
this.transform.y =
|
|
3195
|
+
const dx = current.x - panState.startCanvas.x;
|
|
3196
|
+
const dy = current.y - panState.startCanvas.y;
|
|
3197
|
+
this.transform.x = panState.startTransform.x + dx * this.panScale.x;
|
|
3198
|
+
this.transform.y = panState.startTransform.y + dy * this.panScale.y;
|
|
2544
3199
|
this.applyTransform();
|
|
2545
3200
|
}
|
|
2546
3201
|
onMouseUp(e) {
|
|
2547
|
-
if (
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
3202
|
+
if (this.pendingDrag) {
|
|
3203
|
+
window.clearTimeout(this.pendingDrag.timeout);
|
|
3204
|
+
this.pendingDrag = null;
|
|
3205
|
+
}
|
|
3206
|
+
if (this.editMode.isCreatingEdge) {
|
|
3207
|
+
this.endNewEdge(false);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
if (!this.editMode.isPanning) return;
|
|
3211
|
+
this.editMode.reset();
|
|
2551
3212
|
this.panScale = null;
|
|
2552
3213
|
this.container.style.cursor = "";
|
|
2553
3214
|
}
|
|
@@ -2557,12 +3218,11 @@ var Canvas = class {
|
|
|
2557
3218
|
this.updateZoomLevel();
|
|
2558
3219
|
}
|
|
2559
3220
|
createZoomControls() {
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
/* @__PURE__ */
|
|
2563
|
-
/* @__PURE__ */
|
|
2564
|
-
/* @__PURE__ */
|
|
2565
|
-
/* @__PURE__ */ jsx4("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
|
|
3221
|
+
this.zoomControls = /* @__PURE__ */ jsxs5("div", { className: "g3p-zoom-controls", children: [
|
|
3222
|
+
/* @__PURE__ */ jsx6("button", { className: "g3p-zoom-btn", onClick: () => this.zoomIn(), children: "+" }),
|
|
3223
|
+
/* @__PURE__ */ jsx6("div", { className: "g3p-zoom-level", id: "g3p-zoom-level", children: "100%" }),
|
|
3224
|
+
/* @__PURE__ */ jsx6("button", { className: "g3p-zoom-btn", onClick: () => this.zoomOut(), children: "\u2212" }),
|
|
3225
|
+
/* @__PURE__ */ jsx6("button", { className: "g3p-zoom-btn g3p-zoom-reset", onClick: () => this.zoomReset(), children: "\u27F2" })
|
|
2566
3226
|
] });
|
|
2567
3227
|
this.container.appendChild(this.zoomControls);
|
|
2568
3228
|
}
|
|
@@ -2584,17 +3244,215 @@ var Canvas = class {
|
|
|
2584
3244
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
2585
3245
|
this.applyTransform();
|
|
2586
3246
|
}
|
|
3247
|
+
// ==================== New-Edge Mode ====================
|
|
3248
|
+
/** Start creating a new edge from a node */
|
|
3249
|
+
startNewEdge(sourceNodeId, startGraph, sourcePortId) {
|
|
3250
|
+
this.editMode.startNewEdge(sourceNodeId, startGraph, sourcePortId);
|
|
3251
|
+
this.updateNewEdgeVisual();
|
|
3252
|
+
this.container.style.cursor = "crosshair";
|
|
3253
|
+
}
|
|
3254
|
+
/** Update the new-edge visual during drag */
|
|
3255
|
+
updateNewEdgeVisual() {
|
|
3256
|
+
const state = this.editMode.getNewEdgeState();
|
|
3257
|
+
if (!state) {
|
|
3258
|
+
this.removeNewEdgeVisual();
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (this.newEdgeEl) {
|
|
3262
|
+
this.newEdgeEl.remove();
|
|
3263
|
+
}
|
|
3264
|
+
this.newEdgeEl = renderNewEdge({
|
|
3265
|
+
start: state.startGraph,
|
|
3266
|
+
end: state.currentGraph
|
|
3267
|
+
});
|
|
3268
|
+
this.group.appendChild(this.newEdgeEl);
|
|
3269
|
+
}
|
|
3270
|
+
/** Remove the new-edge visual */
|
|
3271
|
+
removeNewEdgeVisual() {
|
|
3272
|
+
if (this.newEdgeEl) {
|
|
3273
|
+
this.newEdgeEl.remove();
|
|
3274
|
+
this.newEdgeEl = void 0;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
/** Complete or cancel the new-edge creation */
|
|
3278
|
+
endNewEdge(cancelled = false) {
|
|
3279
|
+
const state = this.editMode.getNewEdgeState();
|
|
3280
|
+
if (!state) return;
|
|
3281
|
+
if (!cancelled) {
|
|
3282
|
+
const { target, source } = state;
|
|
3283
|
+
if (target?.type == "node") {
|
|
3284
|
+
this.api.handleAddEdge({ id: "", source, target });
|
|
3285
|
+
} else {
|
|
3286
|
+
this.api.handleNewNodeFrom(source);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
this.removeNewEdgeVisual();
|
|
3290
|
+
this.clearDropTargetHighlight();
|
|
3291
|
+
this.editMode.reset();
|
|
3292
|
+
this.container.style.cursor = "";
|
|
3293
|
+
}
|
|
3294
|
+
/** Find node data by internal ID */
|
|
3295
|
+
findNodeDataById(nodeId) {
|
|
3296
|
+
for (const node of this.curNodes.values()) {
|
|
3297
|
+
if (node.data?.id === nodeId) {
|
|
3298
|
+
return node.data.data;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
return null;
|
|
3302
|
+
}
|
|
3303
|
+
/** Set hover target for new-edge mode */
|
|
3304
|
+
setNewEdgeHoverTarget(id, port) {
|
|
3305
|
+
this.clearDropTargetHighlight();
|
|
3306
|
+
this.editMode.setHoverTarget({ type: "node", id, port });
|
|
3307
|
+
if (port) {
|
|
3308
|
+
const portEl = this.container?.querySelector(`.g3p-node-port[data-node-id="${id}"][data-port-id="${port}"]`);
|
|
3309
|
+
if (portEl) portEl.classList.add("g3p-drop-target");
|
|
3310
|
+
} else {
|
|
3311
|
+
const node = this.curNodes.get(id);
|
|
3312
|
+
if (node?.container) node.container.classList.add("g3p-drop-target");
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
/** Clear hover target for new-edge mode */
|
|
3316
|
+
clearNewEdgeHoverTarget() {
|
|
3317
|
+
this.clearDropTargetHighlight();
|
|
3318
|
+
this.editMode.setHoverTarget(null);
|
|
3319
|
+
}
|
|
3320
|
+
/** Remove drop target highlight from all elements */
|
|
3321
|
+
clearDropTargetHighlight() {
|
|
3322
|
+
for (const node of this.curNodes.values()) {
|
|
3323
|
+
node.container?.classList.remove("g3p-drop-target");
|
|
3324
|
+
}
|
|
3325
|
+
this.container?.querySelectorAll(".g3p-drop-target").forEach((el) => {
|
|
3326
|
+
el.classList.remove("g3p-drop-target");
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
/** Detect hover target during new-edge drag using elementFromPoint */
|
|
3330
|
+
detectHoverTarget(clientX, clientY) {
|
|
3331
|
+
if (this.newEdgeEl) {
|
|
3332
|
+
this.newEdgeEl.style.display = "none";
|
|
3333
|
+
}
|
|
3334
|
+
const el = document.elementFromPoint(clientX, clientY);
|
|
3335
|
+
if (this.newEdgeEl) {
|
|
3336
|
+
this.newEdgeEl.style.display = "";
|
|
3337
|
+
}
|
|
3338
|
+
if (!el) {
|
|
3339
|
+
this.clearNewEdgeHoverTarget();
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
const portEl = el.closest(".g3p-node-port");
|
|
3343
|
+
if (portEl) {
|
|
3344
|
+
const nodeId = portEl.getAttribute("data-node-id");
|
|
3345
|
+
const portId = portEl.getAttribute("data-port-id");
|
|
3346
|
+
if (nodeId && portId) {
|
|
3347
|
+
const node = this.curNodes.get(nodeId);
|
|
3348
|
+
if (node && !node.isDummy) {
|
|
3349
|
+
this.setNewEdgeHoverTarget(nodeId, portId);
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
const nodeEl = el.closest(".g3p-node-container");
|
|
3355
|
+
if (nodeEl) {
|
|
3356
|
+
const nodeId = nodeEl.getAttribute("data-node-id");
|
|
3357
|
+
if (nodeId) {
|
|
3358
|
+
const node = this.curNodes.get(nodeId);
|
|
3359
|
+
if (node && !node.isDummy) {
|
|
3360
|
+
this.setNewEdgeHoverTarget(node.data.id);
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
this.clearNewEdgeHoverTarget();
|
|
3366
|
+
}
|
|
3367
|
+
// ==================== Hit Testing ====================
|
|
3368
|
+
/** Result of a hit test */
|
|
3369
|
+
hitTest(clientX, clientY) {
|
|
3370
|
+
const el = document.elementFromPoint(clientX, clientY);
|
|
3371
|
+
if (!el) return { type: "canvas" };
|
|
3372
|
+
const getCenter = (el2) => {
|
|
3373
|
+
const rect = el2.getBoundingClientRect();
|
|
3374
|
+
return screenPos(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
3375
|
+
};
|
|
3376
|
+
const portEl = el.closest(".g3p-node-port");
|
|
3377
|
+
if (portEl) {
|
|
3378
|
+
const nodeId = portEl.getAttribute("data-node-id");
|
|
3379
|
+
const portId = portEl.getAttribute("data-port-id");
|
|
3380
|
+
if (nodeId && portId) {
|
|
3381
|
+
const center = getCenter(portEl);
|
|
3382
|
+
const node = this.curNodes.get(nodeId);
|
|
3383
|
+
if (node) {
|
|
3384
|
+
return { type: "port", node, port: portId, center };
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
const nodeEl = el.closest(".g3p-node-container");
|
|
3389
|
+
if (nodeEl) {
|
|
3390
|
+
const nodeId = nodeEl.getAttribute("data-node-id");
|
|
3391
|
+
if (nodeId) {
|
|
3392
|
+
const borderEl = el.closest(".g3p-node-border");
|
|
3393
|
+
const center = getCenter(borderEl ?? nodeEl);
|
|
3394
|
+
const node = this.curNodes.get(nodeId);
|
|
3395
|
+
if (node) {
|
|
3396
|
+
return { type: "node", node, center };
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
const edgeEl = el.closest(".g3p-seg-container");
|
|
3401
|
+
if (edgeEl) {
|
|
3402
|
+
const segId = edgeEl.getAttribute("data-edge-id");
|
|
3403
|
+
if (segId) {
|
|
3404
|
+
return { type: "edge", segId };
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
return { type: "canvas" };
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3410
|
+
var themeVarMap = {
|
|
3411
|
+
// Canvas
|
|
3412
|
+
bg: "--g3p-bg",
|
|
3413
|
+
shadow: "--g3p-shadow",
|
|
3414
|
+
// Node
|
|
3415
|
+
border: "--g3p-border",
|
|
3416
|
+
borderHover: "--g3p-border-hover",
|
|
3417
|
+
borderSelected: "--g3p-border-selected",
|
|
3418
|
+
text: "--g3p-text",
|
|
3419
|
+
textMuted: "--g3p-text-muted",
|
|
3420
|
+
// Port
|
|
3421
|
+
bgHover: "--g3p-port-bg-hover",
|
|
3422
|
+
// Edge
|
|
3423
|
+
color: "--g3p-edge-color"
|
|
2587
3424
|
};
|
|
3425
|
+
function themeToCSS(theme, selector, prefix = "") {
|
|
3426
|
+
const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
|
|
3427
|
+
if (!entries.length) return "";
|
|
3428
|
+
let css = `${selector} {
|
|
3429
|
+
`;
|
|
3430
|
+
for (const [key, value] of entries) {
|
|
3431
|
+
let cssVar = themeVarMap[key];
|
|
3432
|
+
if (key === "bg" && prefix === "node") {
|
|
3433
|
+
cssVar = "--g3p-bg-node";
|
|
3434
|
+
} else if (key === "bg" && prefix === "port") {
|
|
3435
|
+
cssVar = "--g3p-port-bg";
|
|
3436
|
+
}
|
|
3437
|
+
if (cssVar) {
|
|
3438
|
+
css += ` ${cssVar}: ${value};
|
|
3439
|
+
`;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
css += "}\n";
|
|
3443
|
+
return css;
|
|
3444
|
+
}
|
|
2588
3445
|
|
|
2589
3446
|
// src/canvas/render-node.tsx
|
|
2590
|
-
import { jsx as
|
|
2591
|
-
function renderNode(node) {
|
|
3447
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "jsx-dom/jsx-runtime";
|
|
3448
|
+
function renderNode(node, props) {
|
|
2592
3449
|
if (typeof node == "string") node = { id: node };
|
|
2593
|
-
const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
|
|
3450
|
+
const title = node?.title ?? props?.title ?? node?.label ?? node?.name ?? node?.text ?? props?.text ?? node?.id ?? "?";
|
|
2594
3451
|
const detail = node?.detail ?? node?.description ?? node?.subtitle;
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3452
|
+
console.log(`renderNode: ${node.id} ${title} ${detail}`);
|
|
3453
|
+
return /* @__PURE__ */ jsxs6("div", { className: "g3p-node-default", children: [
|
|
3454
|
+
/* @__PURE__ */ jsx7("div", { className: "g3p-node-title", children: title }),
|
|
3455
|
+
detail && /* @__PURE__ */ jsx7("div", { className: "g3p-node-detail", children: detail })
|
|
2598
3456
|
] });
|
|
2599
3457
|
}
|
|
2600
3458
|
|
|
@@ -2626,7 +3484,6 @@ function defaults() {
|
|
|
2626
3484
|
},
|
|
2627
3485
|
canvas: {
|
|
2628
3486
|
renderNode,
|
|
2629
|
-
classPrefix: "g3p",
|
|
2630
3487
|
width: "100%",
|
|
2631
3488
|
height: "100%",
|
|
2632
3489
|
padding: 20,
|
|
@@ -2663,26 +3520,50 @@ var Updater = class _Updater {
|
|
|
2663
3520
|
this.update.addNodes.push(node);
|
|
2664
3521
|
return this;
|
|
2665
3522
|
}
|
|
3523
|
+
addNodes(...nodes) {
|
|
3524
|
+
this.update.addNodes.push(...nodes);
|
|
3525
|
+
return this;
|
|
3526
|
+
}
|
|
2666
3527
|
deleteNode(node) {
|
|
2667
3528
|
this.update.removeNodes.push(node);
|
|
2668
3529
|
return this;
|
|
2669
3530
|
}
|
|
3531
|
+
deleteNodes(...nodes) {
|
|
3532
|
+
this.update.removeNodes.push(...nodes);
|
|
3533
|
+
return this;
|
|
3534
|
+
}
|
|
2670
3535
|
updateNode(node) {
|
|
2671
3536
|
this.update.updateNodes.push(node);
|
|
2672
3537
|
return this;
|
|
2673
3538
|
}
|
|
3539
|
+
updateNodes(...nodes) {
|
|
3540
|
+
this.update.updateNodes.push(...nodes);
|
|
3541
|
+
return this;
|
|
3542
|
+
}
|
|
2674
3543
|
addEdge(edge) {
|
|
2675
3544
|
this.update.addEdges.push(edge);
|
|
2676
3545
|
return this;
|
|
2677
3546
|
}
|
|
3547
|
+
addEdges(...edges) {
|
|
3548
|
+
this.update.addEdges.push(...edges);
|
|
3549
|
+
return this;
|
|
3550
|
+
}
|
|
2678
3551
|
deleteEdge(edge) {
|
|
2679
3552
|
this.update.removeEdges.push(edge);
|
|
2680
3553
|
return this;
|
|
2681
3554
|
}
|
|
3555
|
+
deleteEdges(...edges) {
|
|
3556
|
+
this.update.removeEdges.push(...edges);
|
|
3557
|
+
return this;
|
|
3558
|
+
}
|
|
2682
3559
|
updateEdge(edge) {
|
|
2683
3560
|
this.update.updateEdges.push(edge);
|
|
2684
3561
|
return this;
|
|
2685
3562
|
}
|
|
3563
|
+
updateEdges(...edges) {
|
|
3564
|
+
this.update.updateEdges.push(...edges);
|
|
3565
|
+
return this;
|
|
3566
|
+
}
|
|
2686
3567
|
static add(nodes, edges) {
|
|
2687
3568
|
const updater = new _Updater();
|
|
2688
3569
|
updater.update.addNodes = nodes;
|
|
@@ -2703,22 +3584,19 @@ var API = class {
|
|
|
2703
3584
|
nodeIds;
|
|
2704
3585
|
edgeIds;
|
|
2705
3586
|
nodeVersions;
|
|
3587
|
+
nodeOverrides;
|
|
3588
|
+
edgeOverrides;
|
|
3589
|
+
nodeFields;
|
|
2706
3590
|
nextNodeId;
|
|
2707
3591
|
nextEdgeId;
|
|
3592
|
+
events;
|
|
2708
3593
|
root;
|
|
2709
3594
|
constructor(args) {
|
|
2710
3595
|
this.root = args.root;
|
|
2711
3596
|
this.options = applyDefaults(args.options);
|
|
2712
|
-
|
|
2713
|
-
this.
|
|
2714
|
-
this.
|
|
2715
|
-
this.index = 0;
|
|
2716
|
-
this.nodeIds = /* @__PURE__ */ new Map();
|
|
2717
|
-
this.edgeIds = /* @__PURE__ */ new Map();
|
|
2718
|
-
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
2719
|
-
this.nextNodeId = 1;
|
|
2720
|
-
this.nextEdgeId = 1;
|
|
2721
|
-
this.canvas = new Canvas({
|
|
3597
|
+
this.events = args.events || {};
|
|
3598
|
+
this.reset();
|
|
3599
|
+
this.canvas = new Canvas(this, {
|
|
2722
3600
|
...this.options.canvas,
|
|
2723
3601
|
dummyNodeSize: this.options.graph.dummyNodeSize,
|
|
2724
3602
|
orientation: this.options.graph.orientation
|
|
@@ -2731,13 +3609,63 @@ var API = class {
|
|
|
2731
3609
|
this.history = [];
|
|
2732
3610
|
}
|
|
2733
3611
|
}
|
|
3612
|
+
reset() {
|
|
3613
|
+
let graph2 = new Graph({ options: this.options.graph });
|
|
3614
|
+
this.state = { graph: graph2, update: null };
|
|
3615
|
+
this.seq = [this.state];
|
|
3616
|
+
this.index = 0;
|
|
3617
|
+
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3618
|
+
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3619
|
+
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3620
|
+
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3621
|
+
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3622
|
+
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3623
|
+
this.nextNodeId = 1;
|
|
3624
|
+
this.nextEdgeId = 1;
|
|
3625
|
+
}
|
|
3626
|
+
/** Initialize the API */
|
|
2734
3627
|
async init() {
|
|
2735
3628
|
const root = document.getElementById(this.root);
|
|
2736
3629
|
if (!root) throw new Error("root element not found");
|
|
2737
3630
|
root.appendChild(this.canvas.container);
|
|
3631
|
+
await this.applyHistory();
|
|
3632
|
+
}
|
|
3633
|
+
async applyHistory() {
|
|
2738
3634
|
for (const update of this.history)
|
|
2739
3635
|
await this.applyUpdate(update);
|
|
2740
3636
|
}
|
|
3637
|
+
/** Current history index (0-based) */
|
|
3638
|
+
getHistoryIndex() {
|
|
3639
|
+
return this.index;
|
|
3640
|
+
}
|
|
3641
|
+
/** Current history length */
|
|
3642
|
+
getHistoryLength() {
|
|
3643
|
+
return this.seq.length;
|
|
3644
|
+
}
|
|
3645
|
+
/** Toggle canvas editable mode without re-creating the graph */
|
|
3646
|
+
setEditable(editable) {
|
|
3647
|
+
this.canvas.editMode.editable = editable;
|
|
3648
|
+
}
|
|
3649
|
+
/** Replace entire history (clears prior) */
|
|
3650
|
+
async replaceHistory(frames) {
|
|
3651
|
+
this.reset();
|
|
3652
|
+
this.history = frames;
|
|
3653
|
+
await this.applyHistory();
|
|
3654
|
+
}
|
|
3655
|
+
/** Rebuild from snapshot (nodes/edges) */
|
|
3656
|
+
async replaceSnapshot(nodes, edges, description) {
|
|
3657
|
+
this.reset();
|
|
3658
|
+
this.history = [{
|
|
3659
|
+
addNodes: nodes,
|
|
3660
|
+
addEdges: edges,
|
|
3661
|
+
description
|
|
3662
|
+
}];
|
|
3663
|
+
await this.applyHistory();
|
|
3664
|
+
}
|
|
3665
|
+
get graph() {
|
|
3666
|
+
return this.state.graph;
|
|
3667
|
+
}
|
|
3668
|
+
/** Navigate to a different state */
|
|
2741
3669
|
nav(nav) {
|
|
2742
3670
|
let newIndex;
|
|
2743
3671
|
switch (nav) {
|
|
@@ -2759,6 +3687,8 @@ var API = class {
|
|
|
2759
3687
|
this.applyDiff(this.index, newIndex);
|
|
2760
3688
|
this.index = newIndex;
|
|
2761
3689
|
this.state = this.seq[this.index];
|
|
3690
|
+
if (this.events.historyChange)
|
|
3691
|
+
this.events.historyChange(this.index, this.seq.length);
|
|
2762
3692
|
}
|
|
2763
3693
|
applyDiff(oldIndex, newIndex) {
|
|
2764
3694
|
const oldGraph = this.seq[oldIndex].graph;
|
|
@@ -2768,36 +3698,72 @@ var API = class {
|
|
|
2768
3698
|
if (!newNode) this.canvas.deleteNode(oldNode);
|
|
2769
3699
|
}
|
|
2770
3700
|
for (const newNode of newGraph.nodes.values()) {
|
|
2771
|
-
|
|
3701
|
+
const oldNode = oldGraph.nodes.get(newNode.id);
|
|
3702
|
+
if (!oldNode) {
|
|
3703
|
+
this.canvas.addNode(newNode);
|
|
3704
|
+
} else if (oldNode.key !== newNode.key) {
|
|
3705
|
+
this.canvas.deleteNode(oldNode);
|
|
3706
|
+
this.canvas.addNode(newNode);
|
|
3707
|
+
} else if (oldNode.pos !== newNode.pos) {
|
|
3708
|
+
this.canvas.getNode(newNode.key).setPos(newNode.pos);
|
|
3709
|
+
}
|
|
2772
3710
|
}
|
|
2773
3711
|
for (const oldSeg of oldGraph.segs.values()) {
|
|
2774
3712
|
const newSeg = newGraph.segs.get(oldSeg.id);
|
|
2775
|
-
if (!newSeg)
|
|
3713
|
+
if (!newSeg) {
|
|
2776
3714
|
this.canvas.deleteSeg(oldSeg);
|
|
2777
|
-
else if (oldSeg
|
|
2778
|
-
this.canvas.updateSeg(newSeg);
|
|
3715
|
+
} else if (oldSeg !== newSeg) {
|
|
3716
|
+
this.canvas.updateSeg(newSeg, newGraph);
|
|
3717
|
+
}
|
|
2779
3718
|
}
|
|
2780
3719
|
for (const newSeg of newGraph.segs.values()) {
|
|
2781
|
-
if (!oldGraph.segs.has(newSeg.id))
|
|
3720
|
+
if (!oldGraph.segs.has(newSeg.id)) {
|
|
2782
3721
|
this.canvas.addSeg(newSeg, newGraph);
|
|
3722
|
+
}
|
|
2783
3723
|
}
|
|
2784
3724
|
this.canvas.update();
|
|
2785
3725
|
}
|
|
3726
|
+
/** Add a node */
|
|
2786
3727
|
async addNode(node) {
|
|
2787
3728
|
await this.update((update) => update.addNode(node));
|
|
2788
3729
|
}
|
|
3730
|
+
/** Delete a node */
|
|
2789
3731
|
async deleteNode(node) {
|
|
2790
3732
|
await this.update((update) => update.deleteNode(node));
|
|
2791
3733
|
}
|
|
3734
|
+
/** Update a node */
|
|
2792
3735
|
async updateNode(node) {
|
|
2793
3736
|
await this.update((update) => update.updateNode(node));
|
|
2794
3737
|
}
|
|
3738
|
+
/** Add an edge */
|
|
2795
3739
|
async addEdge(edge) {
|
|
2796
3740
|
await this.update((update) => update.addEdge(edge));
|
|
2797
3741
|
}
|
|
3742
|
+
/** Delete an edge */
|
|
2798
3743
|
async deleteEdge(edge) {
|
|
2799
3744
|
await this.update((update) => update.deleteEdge(edge));
|
|
2800
3745
|
}
|
|
3746
|
+
/** Update an edge */
|
|
3747
|
+
async updateEdge(edge) {
|
|
3748
|
+
await this.update((update) => update.updateEdge(edge));
|
|
3749
|
+
}
|
|
3750
|
+
/** Perform a batch of updates */
|
|
3751
|
+
async update(callback) {
|
|
3752
|
+
const updater = new Updater();
|
|
3753
|
+
callback(updater);
|
|
3754
|
+
await this.applyUpdate(updater.update);
|
|
3755
|
+
}
|
|
3756
|
+
/** Rebuild the graph from scratch (removes all then re-adds all nodes/edges) */
|
|
3757
|
+
async rebuild() {
|
|
3758
|
+
const nodes = [...this.nodeIds.keys()];
|
|
3759
|
+
const edges = [...this.edgeIds.keys()];
|
|
3760
|
+
await this.update((updater) => {
|
|
3761
|
+
for (const edge of edges) updater.deleteEdge(edge);
|
|
3762
|
+
for (const node of nodes) updater.deleteNode(node);
|
|
3763
|
+
for (const node of nodes) updater.addNode(node);
|
|
3764
|
+
for (const edge of edges) updater.addEdge(edge);
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
2801
3767
|
async applyUpdate(update) {
|
|
2802
3768
|
log11.info("applyUpdate", update);
|
|
2803
3769
|
const nodes = await this.measureNodes(update);
|
|
@@ -2814,12 +3780,16 @@ var API = class {
|
|
|
2814
3780
|
this._addEdge(edge, mut);
|
|
2815
3781
|
for (const edge of update.updateEdges ?? [])
|
|
2816
3782
|
this._updateEdge(edge, mut);
|
|
3783
|
+
this.nodeOverrides.clear();
|
|
3784
|
+
this.edgeOverrides.clear();
|
|
2817
3785
|
});
|
|
2818
3786
|
this.state = { graph: graph2, update };
|
|
2819
3787
|
this.setNodePositions();
|
|
2820
3788
|
this.seq.splice(this.index + 1);
|
|
2821
3789
|
this.seq.push(this.state);
|
|
2822
3790
|
this.nav("last");
|
|
3791
|
+
if (this.events.historyChange)
|
|
3792
|
+
this.events.historyChange(this.index, this.seq.length);
|
|
2823
3793
|
}
|
|
2824
3794
|
setNodePositions() {
|
|
2825
3795
|
const { graph: graph2 } = this.state;
|
|
@@ -2829,11 +3799,6 @@ var API = class {
|
|
|
2829
3799
|
this.canvas.getNode(node.key).setPos(node.pos);
|
|
2830
3800
|
}
|
|
2831
3801
|
}
|
|
2832
|
-
async update(callback) {
|
|
2833
|
-
const updater = new Updater();
|
|
2834
|
-
callback(updater);
|
|
2835
|
-
await this.applyUpdate(updater.update);
|
|
2836
|
-
}
|
|
2837
3802
|
async measureNodes(update) {
|
|
2838
3803
|
const data = [];
|
|
2839
3804
|
for (const set of [update.updateNodes, update.addNodes])
|
|
@@ -2848,7 +3813,10 @@ var API = class {
|
|
|
2848
3813
|
else if (!data) throw new Error(`invalid node ${data}`);
|
|
2849
3814
|
else if (typeof data == "string") props = { id: data };
|
|
2850
3815
|
else if (typeof data == "object") props = data;
|
|
2851
|
-
else throw new Error(`invalid node ${data}`);
|
|
3816
|
+
else throw new Error(`invalid node ${JSON.stringify(data)}`);
|
|
3817
|
+
this.detectNodeFields(data);
|
|
3818
|
+
const overrides = this.nodeOverrides.get(data);
|
|
3819
|
+
if (overrides) props = { ...props, ...overrides };
|
|
2852
3820
|
let { id, title, text, type, render } = props;
|
|
2853
3821
|
id ??= this.getNodeId(data);
|
|
2854
3822
|
const ports = this.parsePorts(props.ports);
|
|
@@ -2858,6 +3826,21 @@ var API = class {
|
|
|
2858
3826
|
this.nodeVersions.set(data, version);
|
|
2859
3827
|
return { id, data, ports, title, text, type, render, version };
|
|
2860
3828
|
}
|
|
3829
|
+
detectNodeFields(data) {
|
|
3830
|
+
if (typeof data != "object" || !data) return;
|
|
3831
|
+
const skip = /* @__PURE__ */ new Set(["id", "ports", "render", "version"]);
|
|
3832
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3833
|
+
if (skip.has(key)) continue;
|
|
3834
|
+
if (value === null || value === void 0) continue;
|
|
3835
|
+
const type = typeof value;
|
|
3836
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
3837
|
+
this.nodeFields.set(key, type);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
getNodeFields() {
|
|
3842
|
+
return this.nodeFields;
|
|
3843
|
+
}
|
|
2861
3844
|
parseEdge(data) {
|
|
2862
3845
|
const get = this.options.props.edge;
|
|
2863
3846
|
let props;
|
|
@@ -2866,8 +3849,10 @@ var API = class {
|
|
|
2866
3849
|
else if (typeof data == "string") props = this.parseStringEdge(data);
|
|
2867
3850
|
else if (typeof data == "object") props = data;
|
|
2868
3851
|
else throw new Error(`invalid edge ${data}`);
|
|
3852
|
+
const overrides = this.edgeOverrides.get(data);
|
|
3853
|
+
if (overrides) props = { ...props, ...overrides };
|
|
2869
3854
|
let { id, source, target, type } = props;
|
|
2870
|
-
id
|
|
3855
|
+
if (!id) id = this.getEdgeId(data);
|
|
2871
3856
|
source = this.parseEdgeEnd(source);
|
|
2872
3857
|
target = this.parseEdgeEnd(target);
|
|
2873
3858
|
const edge = { id, source, target, type, data };
|
|
@@ -2880,14 +3865,14 @@ var API = class {
|
|
|
2880
3865
|
const keys = Object.keys(end);
|
|
2881
3866
|
const pidx = keys.indexOf("port");
|
|
2882
3867
|
if (pidx != -1) {
|
|
2883
|
-
if (typeof end.port != "string") return end;
|
|
3868
|
+
if (end.port !== void 0 && typeof end.port != "string") return end;
|
|
2884
3869
|
keys.splice(pidx, 1);
|
|
2885
3870
|
}
|
|
2886
3871
|
if (keys.length != 1) return end;
|
|
2887
3872
|
if (keys[0] == "id") return end;
|
|
2888
3873
|
if (keys[0] != "node") return end;
|
|
2889
3874
|
const id = this.nodeIds.get(end.node);
|
|
2890
|
-
if (!id) throw new Error(`edge end
|
|
3875
|
+
if (!id) throw new Error(`edge end references unknown node ${end.node}`);
|
|
2891
3876
|
return { id, port: end.port };
|
|
2892
3877
|
}
|
|
2893
3878
|
throw new Error(`invalid edge end ${end}`);
|
|
@@ -2897,13 +3882,19 @@ var API = class {
|
|
|
2897
3882
|
return { source, target };
|
|
2898
3883
|
}
|
|
2899
3884
|
parsePorts(ports) {
|
|
2900
|
-
const fixed = {
|
|
3885
|
+
const fixed = {};
|
|
2901
3886
|
for (const key of ["in", "out"]) {
|
|
2902
3887
|
if (ports?.[key] && ports[key].length > 0)
|
|
2903
3888
|
fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
|
|
2904
3889
|
}
|
|
2905
3890
|
return fixed;
|
|
2906
3891
|
}
|
|
3892
|
+
getNode(id) {
|
|
3893
|
+
return this.graph.getNode(id);
|
|
3894
|
+
}
|
|
3895
|
+
getEdge(id) {
|
|
3896
|
+
return this.graph.getEdge(id);
|
|
3897
|
+
}
|
|
2907
3898
|
getNodeId(node) {
|
|
2908
3899
|
let id = this.nodeIds.get(node);
|
|
2909
3900
|
if (!id) {
|
|
@@ -2923,7 +3914,6 @@ var API = class {
|
|
|
2923
3914
|
_addNode(node, mut) {
|
|
2924
3915
|
const { data, id: newId } = node.data;
|
|
2925
3916
|
const oldId = this.nodeIds.get(data);
|
|
2926
|
-
console.log("addNode", node, oldId, newId);
|
|
2927
3917
|
if (oldId && oldId != newId)
|
|
2928
3918
|
throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
|
|
2929
3919
|
this.nodeIds.set(data, newId);
|
|
@@ -2931,36 +3921,1152 @@ var API = class {
|
|
|
2931
3921
|
}
|
|
2932
3922
|
_removeNode(node, mut) {
|
|
2933
3923
|
const id = this.nodeIds.get(node);
|
|
2934
|
-
if (!id) throw new Error(`removing node ${node} which does not exist`);
|
|
3924
|
+
if (!id) throw new Error(`removing node ${JSON.stringify(node)} which does not exist`);
|
|
2935
3925
|
mut.removeNode({ id });
|
|
2936
3926
|
}
|
|
2937
3927
|
_updateNode(node, mut) {
|
|
2938
3928
|
const { data, id: newId } = node.data;
|
|
2939
3929
|
const oldId = this.nodeIds.get(data);
|
|
2940
|
-
if (!oldId) throw new Error(`updating unknown node ${node}`);
|
|
2941
|
-
if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
|
|
3930
|
+
if (!oldId) throw new Error(`updating unknown node ${JSON.stringify(node)} `);
|
|
3931
|
+
if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId} `);
|
|
2942
3932
|
mut.updateNode(node.data);
|
|
2943
3933
|
}
|
|
2944
3934
|
_addEdge(edge, mut) {
|
|
2945
3935
|
const data = this.parseEdge(edge);
|
|
2946
3936
|
const id = this.edgeIds.get(edge);
|
|
2947
3937
|
if (id && id != data.id)
|
|
2948
|
-
throw new Error(`edge id changed from ${id} to ${data.id}`);
|
|
3938
|
+
throw new Error(`edge id changed from ${id} to ${data.id} `);
|
|
2949
3939
|
this.edgeIds.set(edge, data.id);
|
|
2950
3940
|
mut.addEdge(data);
|
|
2951
3941
|
}
|
|
2952
3942
|
_removeEdge(edge, mut) {
|
|
2953
3943
|
const id = this.edgeIds.get(edge);
|
|
2954
|
-
if (!id) throw new Error(`removing edge ${edge} which does not exist`);
|
|
3944
|
+
if (!id) throw new Error(`removing edge ${JSON.stringify(edge)} which does not exist`);
|
|
2955
3945
|
mut.removeEdge(this.parseEdge(edge));
|
|
2956
3946
|
}
|
|
2957
3947
|
_updateEdge(edge, mut) {
|
|
2958
3948
|
const id = this.edgeIds.get(edge);
|
|
2959
|
-
if (!id) throw new Error(`updating unknown edge ${edge}`);
|
|
3949
|
+
if (!id) throw new Error(`updating unknown edge ${JSON.stringify(edge)} `);
|
|
2960
3950
|
const data = this.parseEdge(edge);
|
|
2961
|
-
if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
|
|
3951
|
+
if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id} `);
|
|
2962
3952
|
mut.updateEdge(data);
|
|
2963
3953
|
}
|
|
3954
|
+
// Event Handlers
|
|
3955
|
+
handleClickNode(id) {
|
|
3956
|
+
const handler = this.events.nodeClick;
|
|
3957
|
+
const node = this.graph.getNode(id);
|
|
3958
|
+
if (handler) handler(node.data);
|
|
3959
|
+
}
|
|
3960
|
+
handleClickEdge(id) {
|
|
3961
|
+
const handler = this.events.edgeClick;
|
|
3962
|
+
if (!handler) return;
|
|
3963
|
+
const seg = this.graph.getSeg(id);
|
|
3964
|
+
if (seg.edgeIds.size != 1) return;
|
|
3965
|
+
const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
|
|
3966
|
+
handler(edge.data);
|
|
3967
|
+
}
|
|
3968
|
+
async handleNewNode() {
|
|
3969
|
+
const gotNode = async (node) => {
|
|
3970
|
+
await this.addNode(node);
|
|
3971
|
+
};
|
|
3972
|
+
if (this.events.newNode)
|
|
3973
|
+
this.events.newNode(gotNode);
|
|
3974
|
+
else
|
|
3975
|
+
this.canvas.showNewNodeModal(async (data) => {
|
|
3976
|
+
if (this.events.addNode)
|
|
3977
|
+
this.events.addNode(data, gotNode);
|
|
3978
|
+
else
|
|
3979
|
+
await gotNode(data);
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
async handleNewNodeFrom(source) {
|
|
3983
|
+
const gotNode = async (node) => {
|
|
3984
|
+
const gotEdge = async (edge) => {
|
|
3985
|
+
await this.update((u) => {
|
|
3986
|
+
u.addNode(node).addEdge(edge);
|
|
3987
|
+
});
|
|
3988
|
+
};
|
|
3989
|
+
const data = this.graph.getNode(source.id).data;
|
|
3990
|
+
const newEdge = {
|
|
3991
|
+
source: { node: data, port: source.port },
|
|
3992
|
+
target: { node }
|
|
3993
|
+
};
|
|
3994
|
+
if (this.events.addEdge)
|
|
3995
|
+
this.events.addEdge(newEdge, gotEdge);
|
|
3996
|
+
else
|
|
3997
|
+
await gotEdge(newEdge);
|
|
3998
|
+
};
|
|
3999
|
+
if (this.events.newNode)
|
|
4000
|
+
this.events.newNode(gotNode);
|
|
4001
|
+
else
|
|
4002
|
+
this.canvas.showNewNodeModal(async (data) => {
|
|
4003
|
+
if (this.events.addNode)
|
|
4004
|
+
this.events.addNode(data, gotNode);
|
|
4005
|
+
else
|
|
4006
|
+
await gotNode(data);
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
async handleEditNode(id) {
|
|
4010
|
+
const node = this.graph.getNode(id);
|
|
4011
|
+
const gotNode = async (node2) => {
|
|
4012
|
+
if (node2) await this.updateNode(node2);
|
|
4013
|
+
};
|
|
4014
|
+
if (this.events.editNode)
|
|
4015
|
+
this.events.editNode(node.data, gotNode);
|
|
4016
|
+
else {
|
|
4017
|
+
this.canvas.showEditNodeModal(node, async (data) => {
|
|
4018
|
+
if (this.events.updateNode)
|
|
4019
|
+
this.events.updateNode(node.data, data, gotNode);
|
|
4020
|
+
else {
|
|
4021
|
+
this.nodeOverrides.set(node.data, data);
|
|
4022
|
+
await gotNode(node.data);
|
|
4023
|
+
}
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
async handleEditEdge(id) {
|
|
4028
|
+
const seg = this.graph.getSeg(id);
|
|
4029
|
+
if (seg.edgeIds.size != 1) return;
|
|
4030
|
+
const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
|
|
4031
|
+
const gotEdge = async (edge2) => {
|
|
4032
|
+
if (edge2) await this.updateEdge(edge2);
|
|
4033
|
+
};
|
|
4034
|
+
if (this.events.editEdge)
|
|
4035
|
+
this.events.editEdge(edge.data, gotEdge);
|
|
4036
|
+
else
|
|
4037
|
+
this.canvas.showEditEdgeModal(edge, async (data) => {
|
|
4038
|
+
const sourceNode = edge.sourceNode(this.graph);
|
|
4039
|
+
const targetNode = edge.targetNode(this.graph);
|
|
4040
|
+
const update = {
|
|
4041
|
+
source: { node: sourceNode.data, port: data.source.port, marker: data.source.marker },
|
|
4042
|
+
target: { node: targetNode.data, port: data.target.port, marker: data.target.marker }
|
|
4043
|
+
};
|
|
4044
|
+
if (this.events.updateEdge)
|
|
4045
|
+
this.events.updateEdge(edge.data, update, gotEdge);
|
|
4046
|
+
else {
|
|
4047
|
+
this.edgeOverrides.set(edge.data, {
|
|
4048
|
+
source: { id: sourceNode.id, port: data.source.port, marker: data.source.marker },
|
|
4049
|
+
target: { id: targetNode.id, port: data.target.port, marker: data.target.marker },
|
|
4050
|
+
type: data.type
|
|
4051
|
+
});
|
|
4052
|
+
await gotEdge(edge.data);
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4056
|
+
async handleAddEdge(data) {
|
|
4057
|
+
const gotEdge = async (edge) => {
|
|
4058
|
+
if (edge) await this.addEdge(edge);
|
|
4059
|
+
};
|
|
4060
|
+
const newEdge = {
|
|
4061
|
+
source: { node: this.graph.getNode(data.source.id).data, port: data.source.port, marker: data.source.marker },
|
|
4062
|
+
target: { node: this.graph.getNode(data.target.id).data, port: data.target.port, marker: data.target.marker }
|
|
4063
|
+
};
|
|
4064
|
+
if (this.events.addEdge)
|
|
4065
|
+
this.events.addEdge(newEdge, gotEdge);
|
|
4066
|
+
else
|
|
4067
|
+
await gotEdge(data);
|
|
4068
|
+
}
|
|
4069
|
+
async handleDeleteNode(id) {
|
|
4070
|
+
const node = this.getNode(id);
|
|
4071
|
+
if (this.events.removeNode)
|
|
4072
|
+
this.events.removeNode(node.data, async (remove) => {
|
|
4073
|
+
if (remove) await this.deleteNode(node.data);
|
|
4074
|
+
});
|
|
4075
|
+
else
|
|
4076
|
+
await this.deleteNode(node.data);
|
|
4077
|
+
}
|
|
4078
|
+
async handleDeleteEdge(id) {
|
|
4079
|
+
const edge = this.getEdge(id);
|
|
4080
|
+
if (this.events.removeEdge)
|
|
4081
|
+
this.events.removeEdge(edge.data, async (remove) => {
|
|
4082
|
+
if (remove) await this.deleteEdge(edge.data);
|
|
4083
|
+
});
|
|
4084
|
+
else
|
|
4085
|
+
await this.deleteEdge(edge.data);
|
|
4086
|
+
}
|
|
4087
|
+
};
|
|
4088
|
+
|
|
4089
|
+
// src/api/ingest.ts
|
|
4090
|
+
var Ingest = class {
|
|
4091
|
+
constructor(api) {
|
|
4092
|
+
this.api = api;
|
|
4093
|
+
}
|
|
4094
|
+
/**
|
|
4095
|
+
* Apply an incoming ingest message to the API.
|
|
4096
|
+
* - snapshot: rebuild state from nodes/edges (clears prior history)
|
|
4097
|
+
* - update: apply incremental update
|
|
4098
|
+
* - history: initialize from a set of frames (clears prior history)
|
|
4099
|
+
*/
|
|
4100
|
+
async apply(msg) {
|
|
4101
|
+
switch (msg.type) {
|
|
4102
|
+
case "snapshot": {
|
|
4103
|
+
await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
|
|
4104
|
+
break;
|
|
4105
|
+
}
|
|
4106
|
+
case "update": {
|
|
4107
|
+
await this.api.update((u) => {
|
|
4108
|
+
if (msg.addNodes) u.addNodes(...msg.addNodes);
|
|
4109
|
+
if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
|
|
4110
|
+
if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
|
|
4111
|
+
if (msg.addEdges) u.addEdges(...msg.addEdges);
|
|
4112
|
+
if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
|
|
4113
|
+
if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
|
|
4114
|
+
if (msg.description) u.describe(msg.description);
|
|
4115
|
+
});
|
|
4116
|
+
break;
|
|
4117
|
+
}
|
|
4118
|
+
case "history": {
|
|
4119
|
+
await this.api.replaceHistory(msg.frames);
|
|
4120
|
+
break;
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
};
|
|
4125
|
+
|
|
4126
|
+
// src/api/sources/WebSocketSource.ts
|
|
4127
|
+
var WebSocketSource = class {
|
|
4128
|
+
url;
|
|
4129
|
+
ws = null;
|
|
4130
|
+
onMessage;
|
|
4131
|
+
onStatus;
|
|
4132
|
+
reconnectMs;
|
|
4133
|
+
closedByUser = false;
|
|
4134
|
+
connectStartTime = null;
|
|
4135
|
+
totalTimeoutMs = 1e4;
|
|
4136
|
+
totalTimeoutTimer = null;
|
|
4137
|
+
constructor(url, onMessage, onStatus, reconnectMs = 1500) {
|
|
4138
|
+
this.url = url;
|
|
4139
|
+
this.onMessage = onMessage;
|
|
4140
|
+
this.onStatus = onStatus;
|
|
4141
|
+
this.reconnectMs = reconnectMs;
|
|
4142
|
+
}
|
|
4143
|
+
connect() {
|
|
4144
|
+
this.closedByUser = false;
|
|
4145
|
+
this.connectStartTime = Date.now();
|
|
4146
|
+
this.startTotalTimeout();
|
|
4147
|
+
this.open();
|
|
4148
|
+
}
|
|
4149
|
+
disconnect() {
|
|
4150
|
+
this.closedByUser = true;
|
|
4151
|
+
this.clearTotalTimeout();
|
|
4152
|
+
if (this.ws) {
|
|
4153
|
+
try {
|
|
4154
|
+
this.ws.close();
|
|
4155
|
+
} catch {
|
|
4156
|
+
}
|
|
4157
|
+
this.ws = null;
|
|
4158
|
+
}
|
|
4159
|
+
this.onStatus?.("closed");
|
|
4160
|
+
}
|
|
4161
|
+
startTotalTimeout() {
|
|
4162
|
+
this.clearTotalTimeout();
|
|
4163
|
+
this.totalTimeoutTimer = window.setTimeout(() => {
|
|
4164
|
+
if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
|
|
4165
|
+
this.closedByUser = true;
|
|
4166
|
+
if (this.ws) {
|
|
4167
|
+
try {
|
|
4168
|
+
this.ws.close();
|
|
4169
|
+
} catch {
|
|
4170
|
+
}
|
|
4171
|
+
this.ws = null;
|
|
4172
|
+
}
|
|
4173
|
+
this.clearTotalTimeout();
|
|
4174
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4175
|
+
}
|
|
4176
|
+
}, this.totalTimeoutMs);
|
|
4177
|
+
}
|
|
4178
|
+
clearTotalTimeout() {
|
|
4179
|
+
if (this.totalTimeoutTimer !== null) {
|
|
4180
|
+
clearTimeout(this.totalTimeoutTimer);
|
|
4181
|
+
this.totalTimeoutTimer = null;
|
|
4182
|
+
}
|
|
4183
|
+
this.connectStartTime = null;
|
|
4184
|
+
}
|
|
4185
|
+
open() {
|
|
4186
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4187
|
+
if (!this.closedByUser) {
|
|
4188
|
+
this.closedByUser = true;
|
|
4189
|
+
this.clearTotalTimeout();
|
|
4190
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4191
|
+
}
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
this.onStatus?.(this.ws ? "reconnecting" : "connecting");
|
|
4195
|
+
const ws = new WebSocket(this.url);
|
|
4196
|
+
this.ws = ws;
|
|
4197
|
+
ws.onopen = () => {
|
|
4198
|
+
this.clearTotalTimeout();
|
|
4199
|
+
this.onStatus?.("connected");
|
|
4200
|
+
};
|
|
4201
|
+
ws.onerror = (e) => {
|
|
4202
|
+
this.onStatus?.("error", e);
|
|
4203
|
+
};
|
|
4204
|
+
ws.onclose = () => {
|
|
4205
|
+
if (this.closedByUser) {
|
|
4206
|
+
this.onStatus?.("closed");
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4210
|
+
this.closedByUser = true;
|
|
4211
|
+
this.clearTotalTimeout();
|
|
4212
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
this.onStatus?.("reconnecting");
|
|
4216
|
+
setTimeout(() => this.open(), this.reconnectMs);
|
|
4217
|
+
};
|
|
4218
|
+
ws.onmessage = (ev) => {
|
|
4219
|
+
const data = typeof ev.data === "string" ? ev.data : "";
|
|
4220
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4221
|
+
for (const line of lines) {
|
|
4222
|
+
try {
|
|
4223
|
+
const obj = JSON.parse(line);
|
|
4224
|
+
this.onMessage(obj);
|
|
4225
|
+
} catch {
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
};
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4231
|
+
|
|
4232
|
+
// src/api/sources/FileSystemSource.ts
|
|
4233
|
+
var FileSystemSource = class {
|
|
4234
|
+
handle = null;
|
|
4235
|
+
onMessage;
|
|
4236
|
+
onStatus;
|
|
4237
|
+
timer = null;
|
|
4238
|
+
lastSize = 0;
|
|
4239
|
+
filename;
|
|
4240
|
+
intervalMs;
|
|
4241
|
+
constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
|
|
4242
|
+
this.onMessage = onMessage;
|
|
4243
|
+
this.onStatus = onStatus;
|
|
4244
|
+
this.filename = filename;
|
|
4245
|
+
this.intervalMs = intervalMs;
|
|
4246
|
+
}
|
|
4247
|
+
async openDirectory() {
|
|
4248
|
+
try {
|
|
4249
|
+
const dir = await window.showDirectoryPicker?.();
|
|
4250
|
+
if (!dir) throw new Error("File System Access not supported or cancelled");
|
|
4251
|
+
const handle = await dir.getFileHandle(this.filename, { create: false });
|
|
4252
|
+
this.handle = handle;
|
|
4253
|
+
this.onStatus?.("opened", { file: this.filename });
|
|
4254
|
+
this.lastSize = 0;
|
|
4255
|
+
this.startPolling();
|
|
4256
|
+
} catch (e) {
|
|
4257
|
+
this.onStatus?.("error", e);
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
close() {
|
|
4261
|
+
if (this.timer) {
|
|
4262
|
+
window.clearInterval(this.timer);
|
|
4263
|
+
this.timer = null;
|
|
4264
|
+
}
|
|
4265
|
+
this.handle = null;
|
|
4266
|
+
this.onStatus?.("closed");
|
|
4267
|
+
}
|
|
4268
|
+
startPolling() {
|
|
4269
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4270
|
+
this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
|
|
4271
|
+
}
|
|
4272
|
+
async readNewLines() {
|
|
4273
|
+
try {
|
|
4274
|
+
if (!this.handle) return;
|
|
4275
|
+
this.onStatus?.("reading");
|
|
4276
|
+
const file = await this.handle.getFile();
|
|
4277
|
+
if (file.size === this.lastSize) return;
|
|
4278
|
+
const slice = await file.slice(this.lastSize).text();
|
|
4279
|
+
this.lastSize = file.size;
|
|
4280
|
+
const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4281
|
+
for (const line of lines) {
|
|
4282
|
+
try {
|
|
4283
|
+
const obj = JSON.parse(line);
|
|
4284
|
+
this.onMessage(obj);
|
|
4285
|
+
} catch {
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
} catch (e) {
|
|
4289
|
+
this.onStatus?.("error", e);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
};
|
|
4293
|
+
|
|
4294
|
+
// src/api/sources/FileSource.ts
|
|
4295
|
+
var FileSource = class {
|
|
4296
|
+
url;
|
|
4297
|
+
onMessage;
|
|
4298
|
+
onStatus;
|
|
4299
|
+
timer = null;
|
|
4300
|
+
lastETag = null;
|
|
4301
|
+
lastContent = "";
|
|
4302
|
+
intervalMs = 1e3;
|
|
4303
|
+
closed = false;
|
|
4304
|
+
constructor(url, onMessage, onStatus, intervalMs = 1e3) {
|
|
4305
|
+
this.url = url;
|
|
4306
|
+
this.onMessage = onMessage;
|
|
4307
|
+
this.onStatus = onStatus;
|
|
4308
|
+
this.intervalMs = intervalMs;
|
|
4309
|
+
}
|
|
4310
|
+
async connect() {
|
|
4311
|
+
this.closed = false;
|
|
4312
|
+
this.lastETag = null;
|
|
4313
|
+
this.lastContent = "";
|
|
4314
|
+
this.onStatus?.("opened");
|
|
4315
|
+
this.startPolling();
|
|
4316
|
+
}
|
|
4317
|
+
close() {
|
|
4318
|
+
this.closed = true;
|
|
4319
|
+
if (this.timer) {
|
|
4320
|
+
window.clearInterval(this.timer);
|
|
4321
|
+
this.timer = null;
|
|
4322
|
+
}
|
|
4323
|
+
this.onStatus?.("closed");
|
|
4324
|
+
}
|
|
4325
|
+
startPolling() {
|
|
4326
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4327
|
+
this.timer = window.setInterval(() => this.poll(), this.intervalMs);
|
|
4328
|
+
this.poll();
|
|
4329
|
+
}
|
|
4330
|
+
async poll() {
|
|
4331
|
+
if (this.closed) return;
|
|
4332
|
+
try {
|
|
4333
|
+
this.onStatus?.("reading");
|
|
4334
|
+
const headers = {};
|
|
4335
|
+
if (this.lastETag) {
|
|
4336
|
+
headers["If-None-Match"] = this.lastETag;
|
|
4337
|
+
}
|
|
4338
|
+
const response = await fetch(this.url, { headers });
|
|
4339
|
+
if (response.status === 304) {
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
if (!response.ok) {
|
|
4343
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4344
|
+
}
|
|
4345
|
+
const etag = response.headers.get("ETag");
|
|
4346
|
+
if (etag) {
|
|
4347
|
+
this.lastETag = etag;
|
|
4348
|
+
}
|
|
4349
|
+
const content = await response.text();
|
|
4350
|
+
if (content === this.lastContent) {
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4354
|
+
const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4355
|
+
const newLines = lines.slice(lastContentLines.length);
|
|
4356
|
+
for (const line of newLines) {
|
|
4357
|
+
try {
|
|
4358
|
+
const obj = JSON.parse(line);
|
|
4359
|
+
this.onMessage(obj);
|
|
4360
|
+
} catch {
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
this.lastContent = content;
|
|
4364
|
+
} catch (e) {
|
|
4365
|
+
this.onStatus?.("error", e);
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
};
|
|
4369
|
+
|
|
4370
|
+
// src/playground/playground.ts
|
|
4371
|
+
import styles2 from "./styles.css?raw";
|
|
4372
|
+
var Playground = class {
|
|
4373
|
+
options;
|
|
4374
|
+
rootElement;
|
|
4375
|
+
currentExample;
|
|
4376
|
+
currentGraph = null;
|
|
4377
|
+
ingest = null;
|
|
4378
|
+
isEditable = false;
|
|
4379
|
+
wsSource = null;
|
|
4380
|
+
fsSource = null;
|
|
4381
|
+
fileSource = null;
|
|
4382
|
+
wsStatus = "disconnected";
|
|
4383
|
+
fsStatus = "disconnected";
|
|
4384
|
+
fileStatus = "disconnected";
|
|
4385
|
+
activeSourceType = null;
|
|
4386
|
+
wsUrl = "ws://localhost:8787";
|
|
4387
|
+
sourceModal = null;
|
|
4388
|
+
helpOverlay = null;
|
|
4389
|
+
exampleList;
|
|
4390
|
+
graphContainerId;
|
|
4391
|
+
constructor(options) {
|
|
4392
|
+
this.options = options;
|
|
4393
|
+
this.exampleList = Object.keys(options.examples);
|
|
4394
|
+
this.currentExample = options.defaultExample || this.exampleList[0];
|
|
4395
|
+
this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
|
|
4396
|
+
if (typeof options.root === "string") {
|
|
4397
|
+
const el = document.getElementById(options.root);
|
|
4398
|
+
if (!el) throw new Error(`Element with id "${options.root}" not found`);
|
|
4399
|
+
this.rootElement = el;
|
|
4400
|
+
} else {
|
|
4401
|
+
this.rootElement = options.root;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
async init() {
|
|
4405
|
+
this.injectStyles();
|
|
4406
|
+
this.createDOM();
|
|
4407
|
+
this.setupEventListeners();
|
|
4408
|
+
await this.renderGraph();
|
|
4409
|
+
this.updateSourceIcon();
|
|
4410
|
+
this.connectExampleSource();
|
|
4411
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4412
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4413
|
+
}
|
|
4414
|
+
injectStyles() {
|
|
4415
|
+
if (!document.getElementById("g3p-playground-styles")) {
|
|
4416
|
+
const styleEl = document.createElement("style");
|
|
4417
|
+
styleEl.id = "g3p-playground-styles";
|
|
4418
|
+
styleEl.textContent = styles2;
|
|
4419
|
+
document.head.appendChild(styleEl);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
createDOM() {
|
|
4423
|
+
const exampleList = this.exampleList.map((key, i) => {
|
|
4424
|
+
const example = this.options.examples[key];
|
|
4425
|
+
const isActive = i === 0 || key === this.currentExample;
|
|
4426
|
+
return `
|
|
4427
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
4428
|
+
${example.name}
|
|
4429
|
+
</button>
|
|
4430
|
+
`;
|
|
4431
|
+
}).join("");
|
|
4432
|
+
this.rootElement.innerHTML = `
|
|
4433
|
+
<main class="playground">
|
|
4434
|
+
<div class="sidebar">
|
|
4435
|
+
<h2>Examples</h2>
|
|
4436
|
+
<div class="example-list">
|
|
4437
|
+
${exampleList}
|
|
4438
|
+
</div>
|
|
4439
|
+
|
|
4440
|
+
<h2>Options</h2>
|
|
4441
|
+
<div class="options">
|
|
4442
|
+
<div class="option-group">
|
|
4443
|
+
<label>Orientation</label>
|
|
4444
|
+
<select id="orientation">
|
|
4445
|
+
<option value="TB">Top to Bottom</option>
|
|
4446
|
+
<option value="BT">Bottom to Top</option>
|
|
4447
|
+
<option value="LR">Left to Right</option>
|
|
4448
|
+
<option value="RL">Right to Left</option>
|
|
4449
|
+
</select>
|
|
4450
|
+
</div>
|
|
4451
|
+
|
|
4452
|
+
<div class="option-group">
|
|
4453
|
+
<label>Port Style</label>
|
|
4454
|
+
<select id="portStyle">
|
|
4455
|
+
<option value="outside">Outside</option>
|
|
4456
|
+
<option value="inside">Inside</option>
|
|
4457
|
+
</select>
|
|
4458
|
+
</div>
|
|
4459
|
+
|
|
4460
|
+
<div class="option-group">
|
|
4461
|
+
<label>
|
|
4462
|
+
<input type="checkbox" id="portLabelRotate" />
|
|
4463
|
+
Rotate Port Labels
|
|
4464
|
+
</label>
|
|
4465
|
+
</div>
|
|
4466
|
+
|
|
4467
|
+
<div class="option-group">
|
|
4468
|
+
<label>Theme</label>
|
|
4469
|
+
<select id="colorMode">
|
|
4470
|
+
<option value="system">System</option>
|
|
4471
|
+
<option value="light">Light</option>
|
|
4472
|
+
<option value="dark">Dark</option>
|
|
4473
|
+
</select>
|
|
4474
|
+
</div>
|
|
4475
|
+
</div>
|
|
4476
|
+
</div>
|
|
4477
|
+
|
|
4478
|
+
<div class="graph-area">
|
|
4479
|
+
<div class="graph-toolbar">
|
|
4480
|
+
<div class="nav-controls">
|
|
4481
|
+
<button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
|
|
4482
|
+
<button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
|
|
4483
|
+
<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>
|
|
4484
|
+
<button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
|
|
4485
|
+
<button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
|
|
4486
|
+
</div>
|
|
4487
|
+
<div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
|
|
4488
|
+
<button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
|
|
4489
|
+
</div>
|
|
4490
|
+
<button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
|
|
4491
|
+
<button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
|
|
4492
|
+
<button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
|
|
4493
|
+
</div>
|
|
4494
|
+
<div class="graph-container" id="${this.graphContainerId}"></div>
|
|
4495
|
+
</div>
|
|
4496
|
+
</main>
|
|
4497
|
+
`;
|
|
4498
|
+
}
|
|
4499
|
+
setupEventListeners() {
|
|
4500
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
|
|
4501
|
+
btn.addEventListener("click", () => {
|
|
4502
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
4503
|
+
btn.classList.add("active");
|
|
4504
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
4505
|
+
this.renderGraph();
|
|
4506
|
+
this.connectExampleSource();
|
|
4507
|
+
});
|
|
4508
|
+
});
|
|
4509
|
+
this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
|
|
4510
|
+
el.addEventListener("change", () => this.renderGraph());
|
|
4511
|
+
});
|
|
4512
|
+
this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
|
|
4513
|
+
this.currentGraph?.nav("first");
|
|
4514
|
+
this.updateHistoryLabel();
|
|
4515
|
+
});
|
|
4516
|
+
this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
|
|
4517
|
+
this.currentGraph?.nav("prev");
|
|
4518
|
+
this.updateHistoryLabel();
|
|
4519
|
+
});
|
|
4520
|
+
this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
|
|
4521
|
+
this.currentGraph?.nav("next");
|
|
4522
|
+
this.updateHistoryLabel();
|
|
4523
|
+
});
|
|
4524
|
+
this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
|
|
4525
|
+
this.currentGraph?.nav("last");
|
|
4526
|
+
this.updateHistoryLabel();
|
|
4527
|
+
});
|
|
4528
|
+
this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
|
|
4529
|
+
this.currentGraph?.rebuild();
|
|
4530
|
+
});
|
|
4531
|
+
this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
|
|
4532
|
+
this.isEditable = !this.isEditable;
|
|
4533
|
+
const btn = this.rootElement.querySelector("#edit-toggle");
|
|
4534
|
+
if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
|
|
4535
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4536
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4537
|
+
try {
|
|
4538
|
+
this.currentGraph?.setEditable?.(this.isEditable);
|
|
4539
|
+
} catch {
|
|
4540
|
+
}
|
|
4541
|
+
});
|
|
4542
|
+
this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
|
|
4543
|
+
const sourceIconBtn = this.rootElement.querySelector("#source-icon");
|
|
4544
|
+
if (sourceIconBtn) {
|
|
4545
|
+
sourceIconBtn.addEventListener("click", (e) => {
|
|
4546
|
+
e.preventDefault();
|
|
4547
|
+
e.stopPropagation();
|
|
4548
|
+
this.openSourceModal();
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
document.addEventListener("keydown", (e) => {
|
|
4552
|
+
if (!this.currentGraph) return;
|
|
4553
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
|
|
4554
|
+
switch (e.key) {
|
|
4555
|
+
case "Home":
|
|
4556
|
+
this.currentGraph.nav("first");
|
|
4557
|
+
this.updateHistoryLabel();
|
|
4558
|
+
break;
|
|
4559
|
+
case "End":
|
|
4560
|
+
this.currentGraph.nav("last");
|
|
4561
|
+
this.updateHistoryLabel();
|
|
4562
|
+
break;
|
|
4563
|
+
case "ArrowLeft":
|
|
4564
|
+
this.currentGraph.nav("prev");
|
|
4565
|
+
this.updateHistoryLabel();
|
|
4566
|
+
break;
|
|
4567
|
+
case "ArrowRight":
|
|
4568
|
+
this.currentGraph.nav("next");
|
|
4569
|
+
this.updateHistoryLabel();
|
|
4570
|
+
break;
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
4573
|
+
}
|
|
4574
|
+
getResolvedColorMode() {
|
|
4575
|
+
const mode = this.rootElement.querySelector("#colorMode")?.value;
|
|
4576
|
+
if (mode === "system") {
|
|
4577
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4578
|
+
}
|
|
4579
|
+
return mode;
|
|
4580
|
+
}
|
|
4581
|
+
getOptions(exampleOptions) {
|
|
4582
|
+
const orientation = this.rootElement.querySelector("#orientation")?.value;
|
|
4583
|
+
return {
|
|
4584
|
+
graph: { orientation },
|
|
4585
|
+
canvas: {
|
|
4586
|
+
width: "100%",
|
|
4587
|
+
height: "100%",
|
|
4588
|
+
colorMode: this.getResolvedColorMode(),
|
|
4589
|
+
editable: this.isEditable,
|
|
4590
|
+
...exampleOptions?.canvas
|
|
4591
|
+
}
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
async renderGraph() {
|
|
4595
|
+
const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
|
|
4596
|
+
if (!container) return;
|
|
4597
|
+
container.innerHTML = "";
|
|
4598
|
+
const example = this.options.examples[this.currentExample];
|
|
4599
|
+
const options = this.getOptions(example.options);
|
|
4600
|
+
try {
|
|
4601
|
+
this.currentGraph = await graph({
|
|
4602
|
+
root: this.graphContainerId,
|
|
4603
|
+
nodes: example.nodes,
|
|
4604
|
+
edges: example.edges,
|
|
4605
|
+
options,
|
|
4606
|
+
events: {
|
|
4607
|
+
historyChange: () => this.updateHistoryLabel()
|
|
4608
|
+
}
|
|
4609
|
+
});
|
|
4610
|
+
this.ingest = new Ingest(this.currentGraph);
|
|
4611
|
+
this.updateHistoryLabel();
|
|
4612
|
+
} catch (e) {
|
|
4613
|
+
console.error("Failed to render graph:", e);
|
|
4614
|
+
container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
updateHistoryLabel() {
|
|
4618
|
+
const label = this.rootElement.querySelector("#history-label");
|
|
4619
|
+
if (!label || !this.currentGraph) return;
|
|
4620
|
+
try {
|
|
4621
|
+
const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
|
|
4622
|
+
const len = this.currentGraph.getHistoryLength?.() ?? 1;
|
|
4623
|
+
label.textContent = `${idx + 1} / ${len}`;
|
|
4624
|
+
} catch {
|
|
4625
|
+
label.textContent = "\u2014 / \u2014";
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
connectExampleSource() {
|
|
4629
|
+
const example = this.options.examples[this.currentExample];
|
|
4630
|
+
if (!example.source) {
|
|
4631
|
+
this.disconnectAllSources();
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
4634
|
+
this.disconnectAllSources();
|
|
4635
|
+
if (example.source.type === "websocket") {
|
|
4636
|
+
this.wsUrl = example.source.url;
|
|
4637
|
+
this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
4638
|
+
this.wsSource.connect();
|
|
4639
|
+
} else if (example.source.type === "file") {
|
|
4640
|
+
this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
|
|
4641
|
+
this.fileSource.connect();
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
disconnectAllSources() {
|
|
4645
|
+
this.wsSource?.disconnect();
|
|
4646
|
+
this.fsSource?.close();
|
|
4647
|
+
this.fileSource?.close();
|
|
4648
|
+
this.wsSource = null;
|
|
4649
|
+
this.fsSource = null;
|
|
4650
|
+
this.fileSource = null;
|
|
4651
|
+
this.activeSourceType = null;
|
|
4652
|
+
this.wsStatus = "disconnected";
|
|
4653
|
+
this.fsStatus = "disconnected";
|
|
4654
|
+
this.fileStatus = "disconnected";
|
|
4655
|
+
this.updateSourceIcon();
|
|
4656
|
+
}
|
|
4657
|
+
openHelp() {
|
|
4658
|
+
if (!this.helpOverlay) {
|
|
4659
|
+
this.helpOverlay = document.createElement("div");
|
|
4660
|
+
this.helpOverlay.className = "modal-overlay";
|
|
4661
|
+
this.helpOverlay.innerHTML = `
|
|
4662
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
4663
|
+
<div class="modal-header">
|
|
4664
|
+
<h3 id="help-title">Editing the Graph</h3>
|
|
4665
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4666
|
+
</div>
|
|
4667
|
+
<div class="modal-body">
|
|
4668
|
+
<p>Here's how to edit the graph:</p>
|
|
4669
|
+
<ul>
|
|
4670
|
+
<li><strong>Enable editing</strong>: Click "Edit"</li>
|
|
4671
|
+
<li><strong>Add a node</strong>: Double\u2011click an empty area</li>
|
|
4672
|
+
<li><strong>Edit a node</strong>: Double\u2011click a node</li>
|
|
4673
|
+
<li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
|
|
4674
|
+
<li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
|
|
4675
|
+
<li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
|
|
4676
|
+
<li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
|
|
4677
|
+
</ul>
|
|
4678
|
+
<p>When you're done, click "Done" to lock the canvas.</p>
|
|
4679
|
+
</div>
|
|
4680
|
+
</div>
|
|
4681
|
+
`;
|
|
4682
|
+
document.body.appendChild(this.helpOverlay);
|
|
4683
|
+
this.helpOverlay.addEventListener("click", (e) => {
|
|
4684
|
+
const target = e.target;
|
|
4685
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4686
|
+
this.closeHelp();
|
|
4687
|
+
}
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
this.helpOverlay.style.display = "flex";
|
|
4691
|
+
}
|
|
4692
|
+
closeHelp() {
|
|
4693
|
+
if (this.helpOverlay) this.helpOverlay.style.display = "none";
|
|
4694
|
+
}
|
|
4695
|
+
handleIngestMessage = async (msg) => {
|
|
4696
|
+
if (!this.ingest) return;
|
|
4697
|
+
await this.ingest.apply(msg);
|
|
4698
|
+
};
|
|
4699
|
+
updateSourceIcon() {
|
|
4700
|
+
const iconBtn = this.rootElement.querySelector("#source-icon");
|
|
4701
|
+
if (!iconBtn) return;
|
|
4702
|
+
iconBtn.classList.remove("active", "connecting", "error");
|
|
4703
|
+
const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
|
|
4704
|
+
const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
|
|
4705
|
+
const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
|
|
4706
|
+
let icon = "\u{1F4E1}";
|
|
4707
|
+
if (this.activeSourceType === "folder") {
|
|
4708
|
+
icon = "\u{1F4C1}";
|
|
4709
|
+
} else if (this.activeSourceType === "file") {
|
|
4710
|
+
icon = "\u{1F4C4}";
|
|
4711
|
+
}
|
|
4712
|
+
if (isConnected) {
|
|
4713
|
+
iconBtn.classList.add("active");
|
|
4714
|
+
iconBtn.textContent = icon;
|
|
4715
|
+
} else if (isConnecting) {
|
|
4716
|
+
iconBtn.classList.add("connecting");
|
|
4717
|
+
iconBtn.textContent = icon;
|
|
4718
|
+
} else if (hasError) {
|
|
4719
|
+
iconBtn.classList.add("error");
|
|
4720
|
+
iconBtn.textContent = icon;
|
|
4721
|
+
} else {
|
|
4722
|
+
iconBtn.textContent = "\u{1F4E1}";
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
updateWsStatus = (status, detail) => {
|
|
4726
|
+
if (status === "connecting" || status === "reconnecting") {
|
|
4727
|
+
this.wsStatus = "connecting";
|
|
4728
|
+
this.activeSourceType = "ws";
|
|
4729
|
+
} else if (status === "connected") {
|
|
4730
|
+
this.wsStatus = "connected";
|
|
4731
|
+
this.activeSourceType = "ws";
|
|
4732
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4733
|
+
this.fsSource.close();
|
|
4734
|
+
}
|
|
4735
|
+
if (this.fileSource && this.fileStatus === "connected") {
|
|
4736
|
+
this.fileSource.close();
|
|
4737
|
+
}
|
|
4738
|
+
} else if (status === "error") {
|
|
4739
|
+
this.wsStatus = "error";
|
|
4740
|
+
} else {
|
|
4741
|
+
this.wsStatus = "disconnected";
|
|
4742
|
+
if (this.activeSourceType === "ws") {
|
|
4743
|
+
this.activeSourceType = null;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
this.updateSourceIcon();
|
|
4747
|
+
this.updateSourceModal();
|
|
4748
|
+
};
|
|
4749
|
+
updateFsStatus = (status, detail) => {
|
|
4750
|
+
if (status === "opened") {
|
|
4751
|
+
this.fsStatus = "opening";
|
|
4752
|
+
this.activeSourceType = "folder";
|
|
4753
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4754
|
+
this.wsSource.disconnect();
|
|
4755
|
+
}
|
|
4756
|
+
} else if (status === "reading") {
|
|
4757
|
+
this.fsStatus = "connected";
|
|
4758
|
+
this.activeSourceType = "folder";
|
|
4759
|
+
} else if (status === "error") {
|
|
4760
|
+
this.fsStatus = "error";
|
|
4761
|
+
} else if (status === "closed") {
|
|
4762
|
+
this.fsStatus = "disconnected";
|
|
4763
|
+
if (this.activeSourceType === "folder") {
|
|
4764
|
+
this.activeSourceType = null;
|
|
4765
|
+
}
|
|
4766
|
+
} else {
|
|
4767
|
+
this.fsStatus = "disconnected";
|
|
4768
|
+
}
|
|
4769
|
+
this.updateSourceIcon();
|
|
4770
|
+
this.updateSourceModal();
|
|
4771
|
+
};
|
|
4772
|
+
updateFileStatus = (status, detail) => {
|
|
4773
|
+
if (status === "opened") {
|
|
4774
|
+
this.fileStatus = "connecting";
|
|
4775
|
+
this.activeSourceType = "file";
|
|
4776
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4777
|
+
this.wsSource.disconnect();
|
|
4778
|
+
}
|
|
4779
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4780
|
+
this.fsSource.close();
|
|
4781
|
+
}
|
|
4782
|
+
} else if (status === "reading") {
|
|
4783
|
+
this.fileStatus = "connected";
|
|
4784
|
+
this.activeSourceType = "file";
|
|
4785
|
+
} else if (status === "error") {
|
|
4786
|
+
this.fileStatus = "error";
|
|
4787
|
+
} else if (status === "closed") {
|
|
4788
|
+
this.fileStatus = "disconnected";
|
|
4789
|
+
if (this.activeSourceType === "file") {
|
|
4790
|
+
this.activeSourceType = null;
|
|
4791
|
+
}
|
|
4792
|
+
} else {
|
|
4793
|
+
this.fileStatus = "disconnected";
|
|
4794
|
+
}
|
|
4795
|
+
this.updateSourceIcon();
|
|
4796
|
+
this.updateSourceModal();
|
|
4797
|
+
};
|
|
4798
|
+
createSourceModal() {
|
|
4799
|
+
if (this.sourceModal) return this.sourceModal;
|
|
4800
|
+
this.sourceModal = document.createElement("div");
|
|
4801
|
+
this.sourceModal.className = "modal-overlay";
|
|
4802
|
+
this.sourceModal.style.display = "none";
|
|
4803
|
+
this.sourceModal.innerHTML = `
|
|
4804
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
|
|
4805
|
+
<div class="modal-header">
|
|
4806
|
+
<h3 id="source-modal-title">Data Source Connection</h3>
|
|
4807
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4808
|
+
</div>
|
|
4809
|
+
<div class="modal-body">
|
|
4810
|
+
<div class="source-type-selector">
|
|
4811
|
+
<button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
|
|
4812
|
+
<button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
|
|
4813
|
+
</div>
|
|
4814
|
+
|
|
4815
|
+
<div class="source-controls" data-source="ws">
|
|
4816
|
+
<div class="form-group">
|
|
4817
|
+
<label for="source-modal-url">WebSocket URL</label>
|
|
4818
|
+
<input type="text" id="source-modal-url" value="${this.wsUrl}" />
|
|
4819
|
+
</div>
|
|
4820
|
+
<div class="button-group">
|
|
4821
|
+
<button id="source-modal-connect-ws" class="primary">Connect</button>
|
|
4822
|
+
<button id="source-modal-disconnect-ws">Disconnect</button>
|
|
4823
|
+
<button id="source-modal-change-ws">Change Connection</button>
|
|
4824
|
+
</div>
|
|
4825
|
+
</div>
|
|
4826
|
+
|
|
4827
|
+
<div class="source-controls" data-source="folder">
|
|
4828
|
+
<div class="form-group">
|
|
4829
|
+
<label>File System Source</label>
|
|
4830
|
+
<p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
|
|
4831
|
+
Select a directory containing a graph.ndjson file to watch for changes.
|
|
4832
|
+
</p>
|
|
4833
|
+
</div>
|
|
4834
|
+
<div class="button-group">
|
|
4835
|
+
<button id="source-modal-connect-folder" class="primary">Open Folder</button>
|
|
4836
|
+
<button id="source-modal-disconnect-folder">Disconnect</button>
|
|
4837
|
+
</div>
|
|
4838
|
+
</div>
|
|
4839
|
+
|
|
4840
|
+
<div id="source-modal-status"></div>
|
|
4841
|
+
</div>
|
|
4842
|
+
</div>
|
|
4843
|
+
`;
|
|
4844
|
+
document.body.appendChild(this.sourceModal);
|
|
4845
|
+
this.sourceModal.addEventListener("click", (e) => {
|
|
4846
|
+
const target = e.target;
|
|
4847
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4848
|
+
this.closeSourceModal();
|
|
4849
|
+
}
|
|
4850
|
+
});
|
|
4851
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4852
|
+
btn.addEventListener("click", () => {
|
|
4853
|
+
const sourceType = btn.getAttribute("data-source");
|
|
4854
|
+
if (sourceType) {
|
|
4855
|
+
this.selectSourceType(sourceType);
|
|
4856
|
+
}
|
|
4857
|
+
});
|
|
4858
|
+
});
|
|
4859
|
+
document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
|
|
4860
|
+
document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
|
|
4861
|
+
document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
|
|
4862
|
+
document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
|
|
4863
|
+
document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
|
|
4864
|
+
document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
|
|
4865
|
+
if (e.key === "Enter" && this.wsStatus !== "connected") {
|
|
4866
|
+
this.handleConnect();
|
|
4867
|
+
}
|
|
4868
|
+
});
|
|
4869
|
+
return this.sourceModal;
|
|
4870
|
+
}
|
|
4871
|
+
selectSourceType(type, skipUpdate = false) {
|
|
4872
|
+
if (!this.sourceModal) return;
|
|
4873
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4874
|
+
if (btn.getAttribute("data-source") === type) {
|
|
4875
|
+
btn.classList.add("active");
|
|
4876
|
+
} else {
|
|
4877
|
+
btn.classList.remove("active");
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4880
|
+
this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
|
|
4881
|
+
if (controls.getAttribute("data-source") === type) {
|
|
4882
|
+
controls.classList.add("active");
|
|
4883
|
+
} else {
|
|
4884
|
+
controls.classList.remove("active");
|
|
4885
|
+
}
|
|
4886
|
+
});
|
|
4887
|
+
if (!skipUpdate) {
|
|
4888
|
+
this.updateSourceModalContent();
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
updateSourceModalContent() {
|
|
4892
|
+
if (!this.sourceModal) return;
|
|
4893
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
4894
|
+
const connectWsBtn = document.getElementById("source-modal-connect-ws");
|
|
4895
|
+
const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
|
|
4896
|
+
const changeWsBtn = document.getElementById("source-modal-change-ws");
|
|
4897
|
+
if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
|
|
4898
|
+
const isWsConnected = this.wsStatus === "connected";
|
|
4899
|
+
const isWsConnecting = this.wsStatus === "connecting";
|
|
4900
|
+
connectWsBtn.disabled = isWsConnected || isWsConnecting;
|
|
4901
|
+
disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4902
|
+
changeWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4903
|
+
urlInput.disabled = isWsConnecting;
|
|
4904
|
+
}
|
|
4905
|
+
const connectFolderBtn = document.getElementById("source-modal-connect-folder");
|
|
4906
|
+
const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
|
|
4907
|
+
if (connectFolderBtn && disconnectFolderBtn) {
|
|
4908
|
+
const isFolderConnected = this.fsStatus === "connected";
|
|
4909
|
+
const isFolderOpening = this.fsStatus === "opening";
|
|
4910
|
+
connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
|
|
4911
|
+
disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
|
|
4912
|
+
}
|
|
4913
|
+
const statusDiv = document.getElementById("source-modal-status");
|
|
4914
|
+
if (!statusDiv) return;
|
|
4915
|
+
const currentUrl = urlInput?.value || this.wsUrl;
|
|
4916
|
+
statusDiv.innerHTML = "";
|
|
4917
|
+
if (this.activeSourceType === "ws") {
|
|
4918
|
+
if (this.wsStatus === "connecting") {
|
|
4919
|
+
statusDiv.innerHTML = `
|
|
4920
|
+
<div class="status-message info">
|
|
4921
|
+
<span class="loading-spinner"></span>
|
|
4922
|
+
Connecting to ${currentUrl}...
|
|
4923
|
+
</div>
|
|
4924
|
+
`;
|
|
4925
|
+
} else if (this.wsStatus === "connected") {
|
|
4926
|
+
statusDiv.innerHTML = `
|
|
4927
|
+
<div class="status-message success">
|
|
4928
|
+
\u2713 Connected to ${currentUrl}
|
|
4929
|
+
</div>
|
|
4930
|
+
`;
|
|
4931
|
+
} else if (this.wsStatus === "error") {
|
|
4932
|
+
statusDiv.innerHTML = `
|
|
4933
|
+
<div class="status-message error">
|
|
4934
|
+
\u2717 Connection error. Please check the URL and try again.
|
|
4935
|
+
</div>
|
|
4936
|
+
`;
|
|
4937
|
+
} else {
|
|
4938
|
+
statusDiv.innerHTML = `
|
|
4939
|
+
<div class="status-message info">
|
|
4940
|
+
Not connected
|
|
4941
|
+
</div>
|
|
4942
|
+
`;
|
|
4943
|
+
}
|
|
4944
|
+
} else if (this.activeSourceType === "folder") {
|
|
4945
|
+
if (this.fsStatus === "opening") {
|
|
4946
|
+
statusDiv.innerHTML = `
|
|
4947
|
+
<div class="status-message info">
|
|
4948
|
+
<span class="loading-spinner"></span>
|
|
4949
|
+
Opening folder...
|
|
4950
|
+
</div>
|
|
4951
|
+
`;
|
|
4952
|
+
} else if (this.fsStatus === "connected") {
|
|
4953
|
+
statusDiv.innerHTML = `
|
|
4954
|
+
<div class="status-message success">
|
|
4955
|
+
\u2713 Folder connected and watching for changes
|
|
4956
|
+
</div>
|
|
4957
|
+
`;
|
|
4958
|
+
} else if (this.fsStatus === "error") {
|
|
4959
|
+
statusDiv.innerHTML = `
|
|
4960
|
+
<div class="status-message error">
|
|
4961
|
+
\u2717 Error opening folder. Please try again.
|
|
4962
|
+
</div>
|
|
4963
|
+
`;
|
|
4964
|
+
} else {
|
|
4965
|
+
statusDiv.innerHTML = `
|
|
4966
|
+
<div class="status-message info">
|
|
4967
|
+
Not connected
|
|
4968
|
+
</div>
|
|
4969
|
+
`;
|
|
4970
|
+
}
|
|
4971
|
+
} else if (this.activeSourceType === "file") {
|
|
4972
|
+
const example = this.options.examples[this.currentExample];
|
|
4973
|
+
const filePath = example.source?.type === "file" ? example.source.path : "";
|
|
4974
|
+
if (this.fileStatus === "connecting") {
|
|
4975
|
+
statusDiv.innerHTML = `
|
|
4976
|
+
<div class="status-message info">
|
|
4977
|
+
<span class="loading-spinner"></span>
|
|
4978
|
+
Connecting to ${filePath}...
|
|
4979
|
+
</div>
|
|
4980
|
+
`;
|
|
4981
|
+
} else if (this.fileStatus === "connected") {
|
|
4982
|
+
statusDiv.innerHTML = `
|
|
4983
|
+
<div class="status-message success">
|
|
4984
|
+
\u2713 Connected to ${filePath}
|
|
4985
|
+
</div>
|
|
4986
|
+
`;
|
|
4987
|
+
} else if (this.fileStatus === "error") {
|
|
4988
|
+
statusDiv.innerHTML = `
|
|
4989
|
+
<div class="status-message error">
|
|
4990
|
+
\u2717 Error loading file. Please check the path and try again.
|
|
4991
|
+
</div>
|
|
4992
|
+
`;
|
|
4993
|
+
} else {
|
|
4994
|
+
statusDiv.innerHTML = `
|
|
4995
|
+
<div class="status-message info">
|
|
4996
|
+
Not connected
|
|
4997
|
+
</div>
|
|
4998
|
+
`;
|
|
4999
|
+
}
|
|
5000
|
+
} else {
|
|
5001
|
+
statusDiv.innerHTML = `
|
|
5002
|
+
<div class="status-message info">
|
|
5003
|
+
Select a source type to connect
|
|
5004
|
+
</div>
|
|
5005
|
+
`;
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
updateSourceModal() {
|
|
5009
|
+
if (!this.sourceModal) return;
|
|
5010
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5011
|
+
this.selectSourceType(activeType, true);
|
|
5012
|
+
this.updateSourceModalContent();
|
|
5013
|
+
}
|
|
5014
|
+
openSourceModal() {
|
|
5015
|
+
this.createSourceModal();
|
|
5016
|
+
if (this.sourceModal) {
|
|
5017
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5018
|
+
if (urlInput) {
|
|
5019
|
+
urlInput.value = this.wsUrl;
|
|
5020
|
+
}
|
|
5021
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5022
|
+
this.selectSourceType(activeType);
|
|
5023
|
+
this.updateSourceModal();
|
|
5024
|
+
this.sourceModal.style.display = "flex";
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
closeSourceModal() {
|
|
5028
|
+
if (this.sourceModal) {
|
|
5029
|
+
this.sourceModal.style.display = "none";
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
handleConnect() {
|
|
5033
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5034
|
+
if (!urlInput) return;
|
|
5035
|
+
const url = urlInput.value.trim() || "ws://localhost:8787";
|
|
5036
|
+
this.wsUrl = url;
|
|
5037
|
+
if (this.wsSource) {
|
|
5038
|
+
this.wsSource.disconnect();
|
|
5039
|
+
}
|
|
5040
|
+
this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
5041
|
+
this.wsSource.connect();
|
|
5042
|
+
this.updateSourceModal();
|
|
5043
|
+
}
|
|
5044
|
+
handleDisconnect() {
|
|
5045
|
+
this.wsSource?.disconnect();
|
|
5046
|
+
this.updateSourceModal();
|
|
5047
|
+
}
|
|
5048
|
+
handleChangeConnection() {
|
|
5049
|
+
if (this.wsSource) {
|
|
5050
|
+
this.wsSource.disconnect();
|
|
5051
|
+
}
|
|
5052
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5053
|
+
if (urlInput) {
|
|
5054
|
+
urlInput.focus();
|
|
5055
|
+
urlInput.select();
|
|
5056
|
+
}
|
|
5057
|
+
this.updateSourceModal();
|
|
5058
|
+
}
|
|
5059
|
+
async handleOpenFolder() {
|
|
5060
|
+
if (!this.fsSource) {
|
|
5061
|
+
this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
|
|
5062
|
+
}
|
|
5063
|
+
this.updateSourceModal();
|
|
5064
|
+
await this.fsSource.openDirectory();
|
|
5065
|
+
}
|
|
5066
|
+
handleCloseFolder() {
|
|
5067
|
+
this.fsSource?.close();
|
|
5068
|
+
this.updateSourceModal();
|
|
5069
|
+
}
|
|
2964
5070
|
};
|
|
2965
5071
|
|
|
2966
5072
|
// src/index.ts
|
|
@@ -2971,6 +5077,11 @@ async function graph(args = { root: "app" }) {
|
|
|
2971
5077
|
}
|
|
2972
5078
|
var index_default = graph;
|
|
2973
5079
|
export {
|
|
5080
|
+
FileSource,
|
|
5081
|
+
FileSystemSource,
|
|
5082
|
+
Ingest,
|
|
5083
|
+
Playground,
|
|
5084
|
+
WebSocketSource,
|
|
2974
5085
|
index_default as default,
|
|
2975
5086
|
graph
|
|
2976
5087
|
};
|