@kage-core/kage-graph-mcp 1.1.0 → 1.1.1
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/README.md +25 -15
- package/dist/cli.js +64 -7
- package/dist/daemon.js +53 -1
- package/dist/index.js +26 -8
- package/dist/kernel.js +367 -51
- package/package.json +11 -2
- package/viewer/app.js +758 -31
- package/viewer/index.html +28 -3
- package/viewer/styles.css +102 -4
package/viewer/app.js
CHANGED
|
@@ -15,6 +15,21 @@
|
|
|
15
15
|
pendingPackets: [],
|
|
16
16
|
reviewText: "",
|
|
17
17
|
viewBox: { x: 0, y: 0, width: 1000, height: 660 },
|
|
18
|
+
lastVisibleSignature: "",
|
|
19
|
+
sim: {
|
|
20
|
+
nodes: [],
|
|
21
|
+
edges: [],
|
|
22
|
+
nodeById: new Map(),
|
|
23
|
+
panX: 0,
|
|
24
|
+
panY: 0,
|
|
25
|
+
zoom: 1,
|
|
26
|
+
dragNode: null,
|
|
27
|
+
panning: null,
|
|
28
|
+
hoverNode: null,
|
|
29
|
+
raf: null,
|
|
30
|
+
running: false,
|
|
31
|
+
lastSignature: ""
|
|
32
|
+
},
|
|
18
33
|
pan: null
|
|
19
34
|
};
|
|
20
35
|
|
|
@@ -47,10 +62,15 @@
|
|
|
47
62
|
viewMode: document.getElementById("viewMode"),
|
|
48
63
|
typeFilter: document.getElementById("typeFilter"),
|
|
49
64
|
relationFilter: document.getElementById("relationFilter"),
|
|
65
|
+
scopeFilter: document.getElementById("scopeFilter"),
|
|
66
|
+
maxNodes: document.getElementById("maxNodes"),
|
|
67
|
+
showDependencies: document.getElementById("showDependencies"),
|
|
50
68
|
resetView: document.getElementById("resetView"),
|
|
51
69
|
zoomOut: document.getElementById("zoomOut"),
|
|
52
70
|
zoomIn: document.getElementById("zoomIn"),
|
|
53
71
|
fitView: document.getElementById("fitView"),
|
|
72
|
+
canvas: document.getElementById("graphCanvas"),
|
|
73
|
+
tooltip: document.getElementById("graphTooltip"),
|
|
54
74
|
svg: document.getElementById("graphSvg"),
|
|
55
75
|
nodeLayer: document.getElementById("nodeLayer"),
|
|
56
76
|
edgeLayer: document.getElementById("edgeLayer"),
|
|
@@ -72,21 +92,38 @@
|
|
|
72
92
|
els.viewMode.addEventListener("change", render);
|
|
73
93
|
els.typeFilter.addEventListener("change", render);
|
|
74
94
|
els.relationFilter.addEventListener("change", render);
|
|
75
|
-
els.
|
|
76
|
-
els.
|
|
77
|
-
els.
|
|
95
|
+
els.scopeFilter.addEventListener("change", render);
|
|
96
|
+
els.maxNodes.addEventListener("change", render);
|
|
97
|
+
els.showDependencies.addEventListener("change", render);
|
|
98
|
+
els.zoomOut.addEventListener("click", function () { zoomCanvas(0.82); });
|
|
99
|
+
els.zoomIn.addEventListener("click", function () { zoomCanvas(1.22); });
|
|
100
|
+
els.fitView.addEventListener("click", function () { fitCanvas(); drawCanvasGraph(); });
|
|
101
|
+
els.canvas.addEventListener("mousedown", startCanvasPointer);
|
|
102
|
+
els.canvas.addEventListener("mousemove", moveCanvasPointer);
|
|
103
|
+
els.canvas.addEventListener("mouseup", endCanvasPointer);
|
|
104
|
+
els.canvas.addEventListener("mouseleave", leaveCanvasPointer);
|
|
105
|
+
els.canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
|
|
106
|
+
els.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
|
|
78
107
|
els.svg.addEventListener("mousedown", startPan);
|
|
79
108
|
els.svg.addEventListener("click", handleSvgClick);
|
|
80
109
|
els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
|
|
81
110
|
window.addEventListener("mousemove", continuePan);
|
|
82
111
|
window.addEventListener("mouseup", endPan);
|
|
112
|
+
window.addEventListener("resize", function () {
|
|
113
|
+
resizeCanvas();
|
|
114
|
+
fitCanvas();
|
|
115
|
+
drawCanvasGraph();
|
|
116
|
+
});
|
|
83
117
|
els.resetView.addEventListener("click", function () {
|
|
84
118
|
els.searchInput.value = "";
|
|
85
119
|
els.viewMode.value = "combined";
|
|
86
120
|
els.typeFilter.value = "";
|
|
87
121
|
els.relationFilter.value = "";
|
|
122
|
+
els.scopeFilter.value = "signal";
|
|
123
|
+
els.maxNodes.value = "90";
|
|
124
|
+
els.showDependencies.checked = false;
|
|
88
125
|
state.selected = null;
|
|
89
|
-
|
|
126
|
+
state.lastVisibleSignature = "";
|
|
90
127
|
render();
|
|
91
128
|
});
|
|
92
129
|
loadFromUrlParams();
|
|
@@ -134,10 +171,9 @@
|
|
|
134
171
|
return [episode.id, episode];
|
|
135
172
|
}));
|
|
136
173
|
state.selected = null;
|
|
174
|
+
state.lastVisibleSignature = "";
|
|
137
175
|
|
|
138
176
|
populateFilters();
|
|
139
|
-
layoutGraph();
|
|
140
|
-
fitView();
|
|
141
177
|
els.emptyState.classList.add("hidden");
|
|
142
178
|
els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
|
|
143
179
|
render();
|
|
@@ -381,27 +417,32 @@
|
|
|
381
417
|
});
|
|
382
418
|
}
|
|
383
419
|
|
|
384
|
-
function layoutGraph() {
|
|
420
|
+
function layoutGraph(visibleIds) {
|
|
385
421
|
state.positions = new Map();
|
|
422
|
+
var candidates = state.entities.filter(function (entity) {
|
|
423
|
+
return !visibleIds || visibleIds.has(entity.id);
|
|
424
|
+
});
|
|
386
425
|
var lanes = [
|
|
387
|
-
{ name: "memory", x:
|
|
388
|
-
{ name: "files", x:
|
|
389
|
-
{ name: "flow", x:
|
|
390
|
-
{ name: "external", x:
|
|
391
|
-
{ name: "other", x:
|
|
426
|
+
{ name: "memory", x: 170, y: 96, step: 82, columns: 10, match: function (entity) { return entity.graph_kind === "memory"; } },
|
|
427
|
+
{ name: "files", x: 470, y: 86, step: 78, columns: 10, match: function (entity) { return entity.graph_kind === "code" && entity.type === "file"; } },
|
|
428
|
+
{ name: "flow", x: 760, y: 96, step: 76, columns: 10, match: function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; } },
|
|
429
|
+
{ name: "external", x: 1040, y: 122, step: 78, columns: 8, match: function (entity) { return isDependencyEntity(entity) || (entity.graph_kind === "code" && ["external", "script"].indexOf(entity.type) !== -1); } },
|
|
430
|
+
{ name: "other", x: 760, y: 560, step: 78, columns: 8, match: function (entity) { return ["memory", "code"].indexOf(entity.graph_kind) === -1 || (entity.graph_kind === "code" && ["file", "symbol", "route", "test", "external", "script"].indexOf(entity.type) === -1); } }
|
|
392
431
|
];
|
|
432
|
+
var placed = new Set();
|
|
393
433
|
lanes.forEach(function (lane) {
|
|
394
|
-
var bucket =
|
|
395
|
-
return lane.match(entity);
|
|
434
|
+
var bucket = candidates.filter(function (entity) {
|
|
435
|
+
return !placed.has(entity.id) && lane.match(entity);
|
|
396
436
|
}).sort(function (a, b) {
|
|
397
|
-
return
|
|
437
|
+
return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
|
|
398
438
|
});
|
|
399
439
|
if (!bucket.length) return;
|
|
400
440
|
bucket.forEach(function (entity, index) {
|
|
401
|
-
|
|
402
|
-
var
|
|
403
|
-
var
|
|
404
|
-
var
|
|
441
|
+
placed.add(entity.id);
|
|
442
|
+
var column = Math.floor(index / lane.columns);
|
|
443
|
+
var row = index % lane.columns;
|
|
444
|
+
var xOffset = column * 228;
|
|
445
|
+
var yJitter = column % 2 ? 26 : 0;
|
|
405
446
|
state.positions.set(entity.id, {
|
|
406
447
|
x: lane.x + xOffset,
|
|
407
448
|
y: lane.y + row * lane.step + yJitter
|
|
@@ -447,10 +488,23 @@
|
|
|
447
488
|
matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
|
|
448
489
|
}
|
|
449
490
|
|
|
450
|
-
|
|
451
|
-
|
|
491
|
+
var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
|
|
492
|
+
query: query,
|
|
493
|
+
type: type,
|
|
494
|
+
relation: relation,
|
|
495
|
+
scope: els.scopeFilter.value,
|
|
496
|
+
maxNodes: Number(els.maxNodes.value || 90),
|
|
497
|
+
showDependencies: els.showDependencies.checked
|
|
498
|
+
});
|
|
452
499
|
|
|
453
|
-
|
|
500
|
+
state.visibleEntityIds = visible.entities;
|
|
501
|
+
state.visibleEdgeIds = visible.edges;
|
|
502
|
+
layoutGraph(state.visibleEntityIds);
|
|
503
|
+
var nextSignature = visibleSignature(state.visibleEntityIds, state.visibleEdgeIds);
|
|
504
|
+
var graphChanged = nextSignature !== state.lastVisibleSignature;
|
|
505
|
+
state.lastVisibleSignature = nextSignature;
|
|
506
|
+
|
|
507
|
+
renderCanvasGraph(graphChanged);
|
|
454
508
|
renderLists();
|
|
455
509
|
renderDetails();
|
|
456
510
|
renderMetrics();
|
|
@@ -458,6 +512,413 @@
|
|
|
458
512
|
renderProof();
|
|
459
513
|
}
|
|
460
514
|
|
|
515
|
+
function refineVisibleGraph(entityIds, edgeIds, options) {
|
|
516
|
+
var entities = new Set(entityIds);
|
|
517
|
+
var edges = new Set(edgeIds);
|
|
518
|
+
|
|
519
|
+
if (!options.showDependencies) {
|
|
520
|
+
Array.from(entities).forEach(function (id) {
|
|
521
|
+
var entity = state.entityById.get(id);
|
|
522
|
+
if (!entity) return;
|
|
523
|
+
if (isDependencyEntity(entity) && !(options.query && searchableText(entity).indexOf(options.query) !== -1)) {
|
|
524
|
+
entities.delete(id);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
edges = edgesWithVisibleEndpoints(edges, entities);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (options.scope === "focus" && state.selected) {
|
|
531
|
+
var focused = focusSelection(entities, edges, state.selected);
|
|
532
|
+
entities = focused.entities;
|
|
533
|
+
edges = focused.edges;
|
|
534
|
+
} else if (options.scope === "signal" && entities.size > options.maxNodes) {
|
|
535
|
+
var ranked = Array.from(entities).sort(function (a, b) {
|
|
536
|
+
return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
|
|
537
|
+
displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
|
|
538
|
+
});
|
|
539
|
+
entities = new Set(ranked.slice(0, options.maxNodes));
|
|
540
|
+
if (state.selected && state.selected.kind === "entity") entities.add(state.selected.id);
|
|
541
|
+
edges = edgesWithVisibleEndpoints(edges, entities);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (state.selected && state.selected.kind === "edge" && edges.has(state.selected.id)) {
|
|
545
|
+
var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
|
|
546
|
+
if (selectedEdge) {
|
|
547
|
+
entities.add(selectedEdge.from);
|
|
548
|
+
entities.add(selectedEdge.to);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return { entities: entities, edges: edgesWithVisibleEndpoints(edges, entities) };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function focusSelection(entityIds, edgeIds, selection) {
|
|
556
|
+
var entities = new Set();
|
|
557
|
+
var edges = new Set();
|
|
558
|
+
if (selection.kind === "entity") {
|
|
559
|
+
entities.add(selection.id);
|
|
560
|
+
state.edges.forEach(function (edge) {
|
|
561
|
+
if (!edgeIds.has(edge.id)) return;
|
|
562
|
+
if (edge.from === selection.id || edge.to === selection.id) {
|
|
563
|
+
edges.add(edge.id);
|
|
564
|
+
entities.add(edge.from);
|
|
565
|
+
entities.add(edge.to);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
} else {
|
|
569
|
+
var selectedEdge = state.edges.find(function (edge) { return edge.id === selection.id; });
|
|
570
|
+
if (selectedEdge && edgeIds.has(selectedEdge.id)) {
|
|
571
|
+
edges.add(selectedEdge.id);
|
|
572
|
+
entities.add(selectedEdge.from);
|
|
573
|
+
entities.add(selectedEdge.to);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
entityIds.forEach(function (id) {
|
|
577
|
+
if (entities.has(id)) return;
|
|
578
|
+
var entity = state.entityById.get(id);
|
|
579
|
+
if (entity && entity.graph_kind === "memory" && entities.size < 16) entities.add(id);
|
|
580
|
+
});
|
|
581
|
+
return { entities: entities.size ? entities : entityIds, edges: edges.size ? edges : edgeIds };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function edgesWithVisibleEndpoints(edgeIds, entityIds) {
|
|
585
|
+
return new Set(Array.from(edgeIds).filter(function (id) {
|
|
586
|
+
var edge = state.edges.find(function (candidate) { return candidate.id === id; });
|
|
587
|
+
return edge && entityIds.has(edge.from) && entityIds.has(edge.to);
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function renderCanvasGraph(graphChanged) {
|
|
592
|
+
resizeCanvas();
|
|
593
|
+
syncSimulationGraph(graphChanged);
|
|
594
|
+
if (graphChanged) fitCanvas();
|
|
595
|
+
startSimulation();
|
|
596
|
+
drawCanvasGraph();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function resizeCanvas() {
|
|
600
|
+
var canvas = els.canvas;
|
|
601
|
+
var ctx = canvas.getContext("2d");
|
|
602
|
+
var rect = canvas.parentElement.getBoundingClientRect();
|
|
603
|
+
var dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
604
|
+
var width = Math.max(320, rect.width);
|
|
605
|
+
var height = Math.max(360, rect.height);
|
|
606
|
+
if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
|
|
607
|
+
canvas.width = Math.round(width * dpr);
|
|
608
|
+
canvas.height = Math.round(height * dpr);
|
|
609
|
+
canvas.style.width = width + "px";
|
|
610
|
+
canvas.style.height = height + "px";
|
|
611
|
+
}
|
|
612
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function syncSimulationGraph(forceReset) {
|
|
616
|
+
var existing = state.sim.nodeById;
|
|
617
|
+
var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
|
|
618
|
+
var nextNodes = visibleEntities.map(function (entity, index) {
|
|
619
|
+
var current = existing.get(entity.id);
|
|
620
|
+
var lanePos = state.positions.get(entity.id);
|
|
621
|
+
var seed = seededPosition(index, visibleEntities.length);
|
|
622
|
+
var degree = degreeOf(entity.id);
|
|
623
|
+
if (!current || forceReset) {
|
|
624
|
+
current = {
|
|
625
|
+
id: entity.id,
|
|
626
|
+
entity: entity,
|
|
627
|
+
x: lanePos ? lanePos.x : seed.x,
|
|
628
|
+
y: lanePos ? lanePos.y : seed.y,
|
|
629
|
+
vx: 0,
|
|
630
|
+
vy: 0,
|
|
631
|
+
r: clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22)
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
current.entity = entity;
|
|
635
|
+
current.r = clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22);
|
|
636
|
+
return current;
|
|
637
|
+
});
|
|
638
|
+
state.sim.nodes = nextNodes;
|
|
639
|
+
state.sim.nodeById = new Map(nextNodes.map(function (node) { return [node.id, node]; }));
|
|
640
|
+
state.sim.edges = state.edges.filter(function (edge) {
|
|
641
|
+
return state.visibleEdgeIds.has(edge.id) && state.sim.nodeById.has(edge.from) && state.sim.nodeById.has(edge.to);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function seededPosition(index, total) {
|
|
646
|
+
var angle = (Math.PI * 2 * index) / Math.max(1, total);
|
|
647
|
+
var radius = 220 + Math.min(180, total * 2);
|
|
648
|
+
return {
|
|
649
|
+
x: Math.cos(angle) * radius + 520,
|
|
650
|
+
y: Math.sin(angle) * radius + 340
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function startSimulation() {
|
|
655
|
+
if (state.sim.running) return;
|
|
656
|
+
state.sim.running = true;
|
|
657
|
+
state.sim.raf = window.requestAnimationFrame(simulationTick);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function simulationTick() {
|
|
661
|
+
if (!state.sim.running) return;
|
|
662
|
+
stepSimulation();
|
|
663
|
+
drawCanvasGraph();
|
|
664
|
+
state.sim.raf = window.requestAnimationFrame(simulationTick);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function stepSimulation() {
|
|
668
|
+
var nodes = state.sim.nodes;
|
|
669
|
+
if (!nodes.length) return;
|
|
670
|
+
var nodeMap = state.sim.nodeById;
|
|
671
|
+
var count = nodes.length;
|
|
672
|
+
var repulsion = count > 120 ? 3600 : count > 70 ? 2500 : 1700;
|
|
673
|
+
var attraction = count > 100 ? 0.006 : 0.010;
|
|
674
|
+
var centerGravity = count > 100 ? 0.006 : 0.011;
|
|
675
|
+
var laneGravity = 0.012;
|
|
676
|
+
|
|
677
|
+
nodes.forEach(function (node, index) {
|
|
678
|
+
if (state.sim.dragNode === node) return;
|
|
679
|
+
var fx = 0;
|
|
680
|
+
var fy = 0;
|
|
681
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
682
|
+
if (index === j) continue;
|
|
683
|
+
var other = nodes[j];
|
|
684
|
+
var dx = node.x - other.x;
|
|
685
|
+
var dy = node.y - other.y;
|
|
686
|
+
var dist = Math.max(18, Math.sqrt(dx * dx + dy * dy));
|
|
687
|
+
var force = repulsion / (dist * dist);
|
|
688
|
+
fx += (dx / dist) * force;
|
|
689
|
+
fy += (dy / dist) * force;
|
|
690
|
+
}
|
|
691
|
+
var lane = state.positions.get(node.id);
|
|
692
|
+
if (lane) {
|
|
693
|
+
fx += (lane.x - node.x) * laneGravity;
|
|
694
|
+
fy += (lane.y - node.y) * laneGravity;
|
|
695
|
+
}
|
|
696
|
+
fx += (620 - node.x) * centerGravity * 0.15;
|
|
697
|
+
fy += (360 - node.y) * centerGravity * 0.15;
|
|
698
|
+
node.vx = (node.vx + fx) * 0.82;
|
|
699
|
+
node.vy = (node.vy + fy) * 0.82;
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
state.sim.edges.forEach(function (edge) {
|
|
703
|
+
var from = nodeMap.get(edge.from);
|
|
704
|
+
var to = nodeMap.get(edge.to);
|
|
705
|
+
if (!from || !to) return;
|
|
706
|
+
var dx = to.x - from.x;
|
|
707
|
+
var dy = to.y - from.y;
|
|
708
|
+
var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
|
709
|
+
var target = edge.graph_kind === "memory" ? 145 : 120;
|
|
710
|
+
var force = (dist - target) * attraction * (edge.confidence == null ? 1 : clamp(edge.confidence, 0.35, 1.2));
|
|
711
|
+
var fx = (dx / dist) * force;
|
|
712
|
+
var fy = (dy / dist) * force;
|
|
713
|
+
if (state.sim.dragNode !== from) {
|
|
714
|
+
from.vx += fx;
|
|
715
|
+
from.vy += fy;
|
|
716
|
+
}
|
|
717
|
+
if (state.sim.dragNode !== to) {
|
|
718
|
+
to.vx -= fx;
|
|
719
|
+
to.vy -= fy;
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
nodes.forEach(function (node) {
|
|
724
|
+
if (state.sim.dragNode === node) return;
|
|
725
|
+
node.x += clamp(node.vx, -8, 8);
|
|
726
|
+
node.y += clamp(node.vy, -8, 8);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function drawCanvasGraph() {
|
|
731
|
+
var canvas = els.canvas;
|
|
732
|
+
var ctx = canvas.getContext("2d");
|
|
733
|
+
var width = canvas.width / Math.max(1, window.devicePixelRatio || 1);
|
|
734
|
+
var height = canvas.height / Math.max(1, window.devicePixelRatio || 1);
|
|
735
|
+
ctx.clearRect(0, 0, width, height);
|
|
736
|
+
drawCanvasGrid(ctx, width, height);
|
|
737
|
+
ctx.save();
|
|
738
|
+
ctx.translate(state.sim.panX, state.sim.panY);
|
|
739
|
+
ctx.scale(state.sim.zoom, state.sim.zoom);
|
|
740
|
+
drawCanvasEdges(ctx);
|
|
741
|
+
drawCanvasNodes(ctx);
|
|
742
|
+
ctx.restore();
|
|
743
|
+
|
|
744
|
+
if (!state.sim.nodes.length) {
|
|
745
|
+
ctx.fillStyle = "#6ea77d";
|
|
746
|
+
ctx.font = "13px ui-monospace, Menlo, monospace";
|
|
747
|
+
ctx.textAlign = "center";
|
|
748
|
+
ctx.fillText("No graph data visible.", width / 2, height / 2);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function drawCanvasGrid(ctx, width, height) {
|
|
753
|
+
ctx.save();
|
|
754
|
+
ctx.fillStyle = "#020503";
|
|
755
|
+
ctx.fillRect(0, 0, width, height);
|
|
756
|
+
ctx.strokeStyle = "rgba(65,255,143,0.045)";
|
|
757
|
+
ctx.lineWidth = 1;
|
|
758
|
+
var grid = 28;
|
|
759
|
+
for (var x = 0; x < width; x += grid) {
|
|
760
|
+
ctx.beginPath();
|
|
761
|
+
ctx.moveTo(x, 0);
|
|
762
|
+
ctx.lineTo(x, height);
|
|
763
|
+
ctx.stroke();
|
|
764
|
+
}
|
|
765
|
+
for (var y = 0; y < height; y += grid) {
|
|
766
|
+
ctx.beginPath();
|
|
767
|
+
ctx.moveTo(0, y);
|
|
768
|
+
ctx.lineTo(width, y);
|
|
769
|
+
ctx.stroke();
|
|
770
|
+
}
|
|
771
|
+
ctx.restore();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function drawCanvasEdges(ctx) {
|
|
775
|
+
var nodeMap = state.sim.nodeById;
|
|
776
|
+
var focusId = focusedCanvasNodeId();
|
|
777
|
+
var query = normalize(els.searchInput.value);
|
|
778
|
+
var dense = state.sim.nodes.length > 55;
|
|
779
|
+
state.sim.edges.forEach(function (edge) {
|
|
780
|
+
var from = nodeMap.get(edge.from);
|
|
781
|
+
var to = nodeMap.get(edge.to);
|
|
782
|
+
if (!from || !to) return;
|
|
783
|
+
var connected = focusId && (edge.from === focusId || edge.to === focusId);
|
|
784
|
+
var matches = !query || searchableText(edge).indexOf(query) !== -1 || searchableText(from.entity).indexOf(query) !== -1 || searchableText(to.entity).indexOf(query) !== -1;
|
|
785
|
+
var alpha = !matches ? 0.04 : focusId ? (connected ? 0.78 : 0.07) : (dense ? 0.18 : 0.34);
|
|
786
|
+
var color = hexToRgb(palette[from.entity.type] || palette[from.entity.graph_kind] || palette.default);
|
|
787
|
+
var dx = to.x - from.x;
|
|
788
|
+
var dy = to.y - from.y;
|
|
789
|
+
var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
|
790
|
+
var offset = dense ? 10 : 16;
|
|
791
|
+
var cx = (from.x + to.x) / 2 + (-dy / dist) * offset;
|
|
792
|
+
var cy = (from.y + to.y) / 2 + (dx / dist) * offset;
|
|
793
|
+
ctx.beginPath();
|
|
794
|
+
ctx.moveTo(from.x, from.y);
|
|
795
|
+
ctx.quadraticCurveTo(cx, cy, to.x, to.y);
|
|
796
|
+
ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
|
|
797
|
+
ctx.lineWidth = connected ? 2.8 : 1.2;
|
|
798
|
+
ctx.stroke();
|
|
799
|
+
if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
|
|
800
|
+
if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function drawCanvasNodes(ctx) {
|
|
805
|
+
var focusId = focusedCanvasNodeId();
|
|
806
|
+
var query = normalize(els.searchInput.value);
|
|
807
|
+
var dense = state.sim.nodes.length > 55;
|
|
808
|
+
state.sim.nodes.forEach(function (node) {
|
|
809
|
+
var entity = node.entity;
|
|
810
|
+
var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
|
|
811
|
+
var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
|
|
812
|
+
var connected = focusId && (node.id === focusId || state.sim.edges.some(function (edge) {
|
|
813
|
+
return (edge.from === focusId && edge.to === node.id) || (edge.to === focusId && edge.from === node.id);
|
|
814
|
+
}));
|
|
815
|
+
var matches = !query || searchableText(entity).indexOf(query) !== -1;
|
|
816
|
+
var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
|
|
817
|
+
var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
|
|
818
|
+
ctx.save();
|
|
819
|
+
ctx.globalAlpha = alpha;
|
|
820
|
+
if (selected || hovered || (!focusId && !dense)) {
|
|
821
|
+
ctx.shadowColor = color;
|
|
822
|
+
ctx.shadowBlur = selected ? 20 : hovered ? 16 : 5;
|
|
823
|
+
}
|
|
824
|
+
drawNodeShape(ctx, node.x, node.y, node.r, entity);
|
|
825
|
+
var grad = ctx.createRadialGradient(node.x - node.r * 0.3, node.y - node.r * 0.3, 1, node.x, node.y, node.r * 1.35);
|
|
826
|
+
grad.addColorStop(0, brighten(color, 54));
|
|
827
|
+
grad.addColorStop(1, color);
|
|
828
|
+
ctx.fillStyle = grad;
|
|
829
|
+
ctx.fill();
|
|
830
|
+
ctx.restore();
|
|
831
|
+
|
|
832
|
+
if (selected || hovered) {
|
|
833
|
+
ctx.save();
|
|
834
|
+
drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
|
|
835
|
+
ctx.strokeStyle = color;
|
|
836
|
+
ctx.lineWidth = selected ? 3 : 2;
|
|
837
|
+
ctx.shadowColor = color;
|
|
838
|
+
ctx.shadowBlur = 12;
|
|
839
|
+
ctx.stroke();
|
|
840
|
+
ctx.restore();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
var shouldLabel = matches && (selected || hovered || (query && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
|
|
844
|
+
if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function drawArrow(ctx, from, to, cx, cy, color, alpha) {
|
|
849
|
+
var angle = Math.atan2(to.y - cy, to.x - cx);
|
|
850
|
+
var tipX = to.x - to.r * Math.cos(angle);
|
|
851
|
+
var tipY = to.y - to.r * Math.sin(angle);
|
|
852
|
+
ctx.beginPath();
|
|
853
|
+
ctx.moveTo(tipX, tipY);
|
|
854
|
+
ctx.lineTo(tipX - 8 * Math.cos(angle - 0.35), tipY - 8 * Math.sin(angle - 0.35));
|
|
855
|
+
ctx.lineTo(tipX - 8 * Math.cos(angle + 0.35), tipY - 8 * Math.sin(angle + 0.35));
|
|
856
|
+
ctx.closePath();
|
|
857
|
+
ctx.fillStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + Math.min(0.85, alpha + 0.10) + ")";
|
|
858
|
+
ctx.fill();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function drawEdgeLabel(ctx, edge, x, y) {
|
|
862
|
+
var inv = 1 / state.sim.zoom;
|
|
863
|
+
ctx.save();
|
|
864
|
+
ctx.font = "700 " + (10 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
|
|
865
|
+
ctx.textAlign = "center";
|
|
866
|
+
ctx.fillStyle = "rgba(215,249,223,0.82)";
|
|
867
|
+
ctx.fillText(shortName(edge.relation || "related", 22), x, y - 5 * inv);
|
|
868
|
+
ctx.restore();
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function drawNodeLabel(ctx, node, strong) {
|
|
872
|
+
var inv = 1 / state.sim.zoom;
|
|
873
|
+
var label = shortName(displayName(node.entity), strong ? 30 : 20);
|
|
874
|
+
ctx.save();
|
|
875
|
+
ctx.font = (strong ? "800 " : "700 ") + (12 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
|
|
876
|
+
var width = ctx.measureText(label).width + 16 * inv;
|
|
877
|
+
var height = 20 * inv;
|
|
878
|
+
var x = node.x - width / 2;
|
|
879
|
+
var y = node.y + node.r + 8 * inv;
|
|
880
|
+
ctx.fillStyle = "rgba(3,6,4,0.88)";
|
|
881
|
+
roundedRect(ctx, x, y, width, height, 4 * inv);
|
|
882
|
+
ctx.fill();
|
|
883
|
+
ctx.strokeStyle = strong ? "rgba(65,255,143,0.55)" : "rgba(65,255,143,0.18)";
|
|
884
|
+
ctx.stroke();
|
|
885
|
+
ctx.fillStyle = strong ? "#d7f9df" : "#9be7c0";
|
|
886
|
+
ctx.textAlign = "center";
|
|
887
|
+
ctx.fillText(label, node.x, y + 14 * inv);
|
|
888
|
+
ctx.restore();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function drawNodeShape(ctx, x, y, r, entity) {
|
|
892
|
+
var type = entity.type || "";
|
|
893
|
+
if (type === "file" || type === "repo" || type === "command" || type === "script") {
|
|
894
|
+
roundedRect(ctx, x - r * 1.25, y - r * 0.75, r * 2.5, r * 1.5, 4);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (type === "decision" || type === "bug_fix" || type === "test") {
|
|
898
|
+
ctx.beginPath();
|
|
899
|
+
ctx.moveTo(x, y - r);
|
|
900
|
+
ctx.lineTo(x + r, y);
|
|
901
|
+
ctx.lineTo(x, y + r);
|
|
902
|
+
ctx.lineTo(x - r, y);
|
|
903
|
+
ctx.closePath();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (type === "route" || type === "external" || isDependencyEntity(entity)) {
|
|
907
|
+
ctx.beginPath();
|
|
908
|
+
for (var i = 0; i < 6; i++) {
|
|
909
|
+
var angle = Math.PI / 3 * i - Math.PI / 2;
|
|
910
|
+
var px = x + r * Math.cos(angle);
|
|
911
|
+
var py = y + r * Math.sin(angle);
|
|
912
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
913
|
+
else ctx.lineTo(px, py);
|
|
914
|
+
}
|
|
915
|
+
ctx.closePath();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
ctx.beginPath();
|
|
919
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
920
|
+
}
|
|
921
|
+
|
|
461
922
|
function renderSvg() {
|
|
462
923
|
els.edgeLayer.textContent = "";
|
|
463
924
|
els.nodeLayer.textContent = "";
|
|
@@ -467,17 +928,19 @@
|
|
|
467
928
|
var selectedEdgeId = state.selected && state.selected.kind === "edge" ? state.selected.id : null;
|
|
468
929
|
var connectedIds = connectedEntityIds(selectedEntityId, selectedEdgeId);
|
|
469
930
|
|
|
931
|
+
renderLaneLabels();
|
|
932
|
+
|
|
470
933
|
state.edges.forEach(function (edge) {
|
|
934
|
+
if (!state.visibleEdgeIds.has(edge.id)) return;
|
|
471
935
|
var from = state.positions.get(edge.from);
|
|
472
936
|
var to = state.positions.get(edge.to);
|
|
473
937
|
if (!from || !to) return;
|
|
474
938
|
|
|
475
939
|
var group = svgEl("g");
|
|
476
|
-
var visible = state.visibleEdgeIds.has(edge.id);
|
|
477
940
|
var connected = selectedEdgeId === edge.id || connectedIds.edges.has(edge.id);
|
|
478
941
|
var line = svgEl("path", {
|
|
479
942
|
d: edgePath(from, to),
|
|
480
|
-
class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"),
|
|
943
|
+
class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), connected && "connected", selectedEdgeId === edge.id && "selected")
|
|
481
944
|
});
|
|
482
945
|
var hit = svgEl("path", {
|
|
483
946
|
d: edgePath(from, to),
|
|
@@ -496,14 +959,14 @@
|
|
|
496
959
|
});
|
|
497
960
|
|
|
498
961
|
state.entities.forEach(function (entity) {
|
|
962
|
+
if (!state.visibleEntityIds.has(entity.id)) return;
|
|
499
963
|
var pos = state.positions.get(entity.id);
|
|
500
964
|
if (!pos) return;
|
|
501
965
|
|
|
502
|
-
var visible = state.visibleEntityIds.has(entity.id);
|
|
503
966
|
var selected = selectedEntityId === entity.id;
|
|
504
967
|
var connected = connectedIds.entities.has(entity.id);
|
|
505
968
|
var group = svgEl("g", {
|
|
506
|
-
class: classNames("node", "graph-" + (entity.graph_kind || "unknown"),
|
|
969
|
+
class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), "type-" + safeCssName(entity.type || "unknown"), isDependencyEntity(entity) && "dependency-node", selected && "selected", connected && "connected"),
|
|
507
970
|
transform: "translate(" + pos.x + " " + pos.y + ")"
|
|
508
971
|
});
|
|
509
972
|
var dims = nodeDimensions(entity);
|
|
@@ -551,6 +1014,21 @@
|
|
|
551
1014
|
});
|
|
552
1015
|
}
|
|
553
1016
|
|
|
1017
|
+
function renderLaneLabels() {
|
|
1018
|
+
var visible = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
|
|
1019
|
+
[
|
|
1020
|
+
["MEMORY", 92, visible.some(function (entity) { return entity.graph_kind === "memory"; })],
|
|
1021
|
+
["FILES", 392, visible.some(function (entity) { return entity.graph_kind === "code" && entity.type === "file"; })],
|
|
1022
|
+
["FLOW", 682, visible.some(function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; })],
|
|
1023
|
+
["DEPS", 962, visible.some(function (entity) { return isDependencyEntity(entity); })]
|
|
1024
|
+
].forEach(function (lane) {
|
|
1025
|
+
if (!lane[2]) return;
|
|
1026
|
+
var label = svgEl("text", { x: lane[1], y: 34, class: "lane-label" });
|
|
1027
|
+
label.textContent = lane[0];
|
|
1028
|
+
els.edgeLayer.appendChild(label);
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
554
1032
|
function renderLists() {
|
|
555
1033
|
var visibleEntities = state.entities.filter(function (entity) {
|
|
556
1034
|
return state.visibleEntityIds.has(entity.id);
|
|
@@ -688,6 +1166,9 @@
|
|
|
688
1166
|
if (!els.metricsSummary) return;
|
|
689
1167
|
var visibleEdges = state.edges.filter(function (edge) { return state.visibleEdgeIds.has(edge.id); });
|
|
690
1168
|
var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
|
|
1169
|
+
var hiddenDependencies = state.entities.filter(function (entity) {
|
|
1170
|
+
return isDependencyEntity(entity) && !state.visibleEntityIds.has(entity.id);
|
|
1171
|
+
}).length;
|
|
691
1172
|
var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
|
|
692
1173
|
var official = state.metrics;
|
|
693
1174
|
var metrics = official ? [
|
|
@@ -716,7 +1197,8 @@
|
|
|
716
1197
|
});
|
|
717
1198
|
renderStatusStrip(visibleEntities, visibleEdges, official);
|
|
718
1199
|
els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
|
|
719
|
-
els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations
|
|
1200
|
+
els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
|
|
1201
|
+
(hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
|
|
720
1202
|
}
|
|
721
1203
|
|
|
722
1204
|
function renderReviewQueue() {
|
|
@@ -839,6 +1321,212 @@
|
|
|
839
1321
|
}, 0);
|
|
840
1322
|
}
|
|
841
1323
|
|
|
1324
|
+
function entityImportance(entity) {
|
|
1325
|
+
if (!entity) return -1000;
|
|
1326
|
+
var score = degreeOf(entity.id) * 8;
|
|
1327
|
+
if (entity.graph_kind === "memory") score += 80;
|
|
1328
|
+
if (entity.graph_kind === "code" && entity.type === "file") score += 42;
|
|
1329
|
+
if (["route", "test"].indexOf(entity.type) !== -1) score += 46;
|
|
1330
|
+
if (entity.type === "symbol") score += 34;
|
|
1331
|
+
if (entity.type === "script") score += 24;
|
|
1332
|
+
if (["runbook", "bug_fix", "decision", "workflow", "gotcha", "convention"].indexOf(entity.type) !== -1) score += 40;
|
|
1333
|
+
if (isDependencyEntity(entity)) score -= 90;
|
|
1334
|
+
if (isGeneratedEntity(entity)) score -= 48;
|
|
1335
|
+
return score;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function isDependencyEntity(entity) {
|
|
1339
|
+
var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
|
|
1340
|
+
return entity.type === "external" ||
|
|
1341
|
+
text.indexOf("node_modules/") !== -1 ||
|
|
1342
|
+
text.indexOf("/node_modules") !== -1 ||
|
|
1343
|
+
text.indexOf("package-lock.json") !== -1 ||
|
|
1344
|
+
text.indexOf("pnpm-lock.yaml") !== -1 ||
|
|
1345
|
+
text.indexOf("yarn.lock") !== -1 ||
|
|
1346
|
+
text.indexOf("bun.lockb") !== -1 ||
|
|
1347
|
+
text.indexOf("dist/") !== -1 ||
|
|
1348
|
+
text.indexOf("build/") !== -1;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function isGeneratedEntity(entity) {
|
|
1352
|
+
var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
|
|
1353
|
+
return text.indexOf(".agent_memory/indexes/") !== -1 ||
|
|
1354
|
+
text.indexOf(".agent_memory/code_graph/") !== -1 ||
|
|
1355
|
+
text.indexOf(".agent_memory/graph/") !== -1 ||
|
|
1356
|
+
text.indexOf(".min.js") !== -1 ||
|
|
1357
|
+
text.indexOf("coverage/") !== -1;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function startCanvasPointer(event) {
|
|
1361
|
+
if (event.button !== 0) return;
|
|
1362
|
+
var world = canvasToWorld(event);
|
|
1363
|
+
var node = findCanvasNode(world.x, world.y);
|
|
1364
|
+
if (node) {
|
|
1365
|
+
state.sim.dragNode = node;
|
|
1366
|
+
state.sim.panning = null;
|
|
1367
|
+
} else {
|
|
1368
|
+
state.sim.panning = {
|
|
1369
|
+
x: event.clientX,
|
|
1370
|
+
y: event.clientY,
|
|
1371
|
+
panX: state.sim.panX,
|
|
1372
|
+
panY: state.sim.panY,
|
|
1373
|
+
moved: false
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function moveCanvasPointer(event) {
|
|
1379
|
+
var world = canvasToWorld(event);
|
|
1380
|
+
if (state.sim.dragNode) {
|
|
1381
|
+
state.sim.dragNode.x = world.x;
|
|
1382
|
+
state.sim.dragNode.y = world.y;
|
|
1383
|
+
state.sim.dragNode.vx = 0;
|
|
1384
|
+
state.sim.dragNode.vy = 0;
|
|
1385
|
+
} else if (state.sim.panning) {
|
|
1386
|
+
var dx = event.clientX - state.sim.panning.x;
|
|
1387
|
+
var dy = event.clientY - state.sim.panning.y;
|
|
1388
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.sim.panning.moved = true;
|
|
1389
|
+
state.sim.panX = state.sim.panning.panX + dx;
|
|
1390
|
+
state.sim.panY = state.sim.panning.panY + dy;
|
|
1391
|
+
}
|
|
1392
|
+
state.sim.hoverNode = findCanvasNode(world.x, world.y);
|
|
1393
|
+
updateCanvasTooltip(event);
|
|
1394
|
+
drawCanvasGraph();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function endCanvasPointer() {
|
|
1398
|
+
if (state.sim.dragNode) {
|
|
1399
|
+
state.selected = { kind: "entity", id: state.sim.dragNode.id };
|
|
1400
|
+
state.sim.dragNode = null;
|
|
1401
|
+
render();
|
|
1402
|
+
} else if (state.sim.panning && !state.sim.panning.moved) {
|
|
1403
|
+
state.selected = null;
|
|
1404
|
+
render();
|
|
1405
|
+
}
|
|
1406
|
+
state.sim.panning = null;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function leaveCanvasPointer() {
|
|
1410
|
+
state.sim.hoverNode = null;
|
|
1411
|
+
state.sim.dragNode = null;
|
|
1412
|
+
state.sim.panning = null;
|
|
1413
|
+
if (els.tooltip) els.tooltip.classList.remove("visible");
|
|
1414
|
+
drawCanvasGraph();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function handleCanvasWheel(event) {
|
|
1418
|
+
event.preventDefault();
|
|
1419
|
+
var rect = els.canvas.getBoundingClientRect();
|
|
1420
|
+
var before = canvasToWorld(event);
|
|
1421
|
+
var factor = event.deltaY > 0 ? 0.88 : 1.14;
|
|
1422
|
+
state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
|
|
1423
|
+
state.sim.panX = event.clientX - rect.left - before.x * state.sim.zoom;
|
|
1424
|
+
state.sim.panY = event.clientY - rect.top - before.y * state.sim.zoom;
|
|
1425
|
+
drawCanvasGraph();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function handleCanvasDoubleClick(event) {
|
|
1429
|
+
var world = canvasToWorld(event);
|
|
1430
|
+
var node = findCanvasNode(world.x, world.y);
|
|
1431
|
+
if (!node) return;
|
|
1432
|
+
state.selected = { kind: "entity", id: node.id };
|
|
1433
|
+
els.scopeFilter.value = "focus";
|
|
1434
|
+
render();
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function canvasToWorld(event) {
|
|
1438
|
+
var rect = els.canvas.getBoundingClientRect();
|
|
1439
|
+
return {
|
|
1440
|
+
x: (event.clientX - rect.left - state.sim.panX) / state.sim.zoom,
|
|
1441
|
+
y: (event.clientY - rect.top - state.sim.panY) / state.sim.zoom
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function findCanvasNode(x, y) {
|
|
1446
|
+
for (var i = state.sim.nodes.length - 1; i >= 0; i--) {
|
|
1447
|
+
var node = state.sim.nodes[i];
|
|
1448
|
+
var dx = node.x - x;
|
|
1449
|
+
var dy = node.y - y;
|
|
1450
|
+
if (dx * dx + dy * dy <= Math.pow(node.r + 7, 2)) return node;
|
|
1451
|
+
}
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function updateCanvasTooltip(event) {
|
|
1456
|
+
if (!els.tooltip) return;
|
|
1457
|
+
var node = state.sim.hoverNode;
|
|
1458
|
+
if (!node || state.sim.dragNode || state.sim.panning) {
|
|
1459
|
+
els.tooltip.classList.remove("visible");
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
var entity = node.entity;
|
|
1463
|
+
var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
|
|
1464
|
+
var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
|
|
1465
|
+
els.tooltip.innerHTML = [
|
|
1466
|
+
"<div class=\"tt-name\"></div>",
|
|
1467
|
+
"<div class=\"tt-type\"></div>",
|
|
1468
|
+
"<div class=\"tt-summary\"></div>",
|
|
1469
|
+
"<div class=\"tt-conns\"></div>"
|
|
1470
|
+
].join("");
|
|
1471
|
+
els.tooltip.querySelector(".tt-name").textContent = displayName(entity);
|
|
1472
|
+
els.tooltip.querySelector(".tt-type").textContent = nodeKindLabel(entity);
|
|
1473
|
+
els.tooltip.querySelector(".tt-type").style.color = color;
|
|
1474
|
+
els.tooltip.querySelector(".tt-summary").textContent = shortName(entity.summary || entity.id, 150);
|
|
1475
|
+
els.tooltip.querySelector(".tt-conns").textContent = relationCount + " relation" + (relationCount === 1 ? "" : "s");
|
|
1476
|
+
var rect = els.canvas.getBoundingClientRect();
|
|
1477
|
+
els.tooltip.style.left = (event.clientX - rect.left + 14) + "px";
|
|
1478
|
+
els.tooltip.style.top = (event.clientY - rect.top + 14) + "px";
|
|
1479
|
+
els.tooltip.classList.add("visible");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function zoomCanvas(factor) {
|
|
1483
|
+
resizeCanvas();
|
|
1484
|
+
var rect = els.canvas.getBoundingClientRect();
|
|
1485
|
+
var cx = rect.width / 2;
|
|
1486
|
+
var cy = rect.height / 2;
|
|
1487
|
+
var before = {
|
|
1488
|
+
x: (cx - state.sim.panX) / state.sim.zoom,
|
|
1489
|
+
y: (cy - state.sim.panY) / state.sim.zoom
|
|
1490
|
+
};
|
|
1491
|
+
state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
|
|
1492
|
+
state.sim.panX = cx - before.x * state.sim.zoom;
|
|
1493
|
+
state.sim.panY = cy - before.y * state.sim.zoom;
|
|
1494
|
+
drawCanvasGraph();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function fitCanvas() {
|
|
1498
|
+
resizeCanvas();
|
|
1499
|
+
if (!state.sim.nodes.length) {
|
|
1500
|
+
state.sim.panX = 0;
|
|
1501
|
+
state.sim.panY = 0;
|
|
1502
|
+
state.sim.zoom = 1;
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
var xs = state.sim.nodes.map(function (node) { return node.x; });
|
|
1506
|
+
var ys = state.sim.nodes.map(function (node) { return node.y; });
|
|
1507
|
+
var minX = Math.min.apply(null, xs) - 90;
|
|
1508
|
+
var maxX = Math.max.apply(null, xs) + 90;
|
|
1509
|
+
var minY = Math.min.apply(null, ys) - 90;
|
|
1510
|
+
var maxY = Math.max.apply(null, ys) + 90;
|
|
1511
|
+
var width = els.canvas.width / Math.max(1, window.devicePixelRatio || 1);
|
|
1512
|
+
var height = els.canvas.height / Math.max(1, window.devicePixelRatio || 1);
|
|
1513
|
+
var graphWidth = Math.max(1, maxX - minX);
|
|
1514
|
+
var graphHeight = Math.max(1, maxY - minY);
|
|
1515
|
+
state.sim.zoom = clamp(Math.min(width / graphWidth, height / graphHeight), 0.22, 1.45);
|
|
1516
|
+
state.sim.panX = width / 2 - ((minX + maxX) / 2) * state.sim.zoom;
|
|
1517
|
+
state.sim.panY = height / 2 - ((minY + maxY) / 2) * state.sim.zoom;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function focusedCanvasNodeId() {
|
|
1521
|
+
if (state.sim.hoverNode) return state.sim.hoverNode.id;
|
|
1522
|
+
if (state.selected && state.selected.kind === "entity") return state.selected.id;
|
|
1523
|
+
if (state.selected && state.selected.kind === "edge") {
|
|
1524
|
+
var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
|
|
1525
|
+
return selectedEdge ? selectedEdge.from : null;
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
842
1530
|
function fitView() {
|
|
843
1531
|
var visible = state.entities.filter(function (entity) {
|
|
844
1532
|
return state.visibleEntityIds.size === 0 || state.visibleEntityIds.has(entity.id);
|
|
@@ -855,10 +1543,10 @@
|
|
|
855
1543
|
var minY = Math.min.apply(null, ys) - 82;
|
|
856
1544
|
var maxY = Math.max.apply(null, ys) + 82;
|
|
857
1545
|
state.viewBox = {
|
|
858
|
-
x:
|
|
859
|
-
y:
|
|
860
|
-
width: Math.max(
|
|
861
|
-
height: Math.max(
|
|
1546
|
+
x: minX,
|
|
1547
|
+
y: minY,
|
|
1548
|
+
width: Math.max(620, maxX - minX),
|
|
1549
|
+
height: Math.max(400, maxY - minY)
|
|
862
1550
|
};
|
|
863
1551
|
}
|
|
864
1552
|
|
|
@@ -982,10 +1670,49 @@
|
|
|
982
1670
|
return Math.max(min, Math.min(max, value));
|
|
983
1671
|
}
|
|
984
1672
|
|
|
1673
|
+
function hexToRgb(value) {
|
|
1674
|
+
var hex = String(value || "#9be7c0").replace("#", "");
|
|
1675
|
+
if (hex.length === 3) hex = hex.split("").map(function (part) { return part + part; }).join("");
|
|
1676
|
+
var parsed = parseInt(hex, 16);
|
|
1677
|
+
return {
|
|
1678
|
+
r: (parsed >> 16) & 255,
|
|
1679
|
+
g: (parsed >> 8) & 255,
|
|
1680
|
+
b: parsed & 255
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function brighten(value, amount) {
|
|
1685
|
+
var rgb = hexToRgb(value);
|
|
1686
|
+
return "rgb(" + Math.min(255, rgb.r + amount) + "," + Math.min(255, rgb.g + amount) + "," + Math.min(255, rgb.b + amount) + ")";
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function roundedRect(ctx, x, y, width, height, radius) {
|
|
1690
|
+
var r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2);
|
|
1691
|
+
ctx.beginPath();
|
|
1692
|
+
ctx.moveTo(x + r, y);
|
|
1693
|
+
ctx.lineTo(x + width - r, y);
|
|
1694
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
1695
|
+
ctx.lineTo(x + width, y + height - r);
|
|
1696
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
1697
|
+
ctx.lineTo(x + r, y + height);
|
|
1698
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
1699
|
+
ctx.lineTo(x, y + r);
|
|
1700
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
1701
|
+
ctx.closePath();
|
|
1702
|
+
}
|
|
1703
|
+
|
|
985
1704
|
function classNames() {
|
|
986
1705
|
return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
|
|
987
1706
|
}
|
|
988
1707
|
|
|
1708
|
+
function visibleSignature(entityIds, edgeIds) {
|
|
1709
|
+
return Array.from(entityIds).sort().join("|") + "::" + Array.from(edgeIds).sort().join("|");
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function safeCssName(value) {
|
|
1713
|
+
return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
1714
|
+
}
|
|
1715
|
+
|
|
989
1716
|
function svgEl(name, attrs) {
|
|
990
1717
|
var element = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
991
1718
|
Object.keys(attrs || {}).forEach(function (key) {
|