laminark 0.1.0 → 2.21.7
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/.claude-plugin/marketplace.json +15 -0
- package/README.md +71 -36
- package/package.json +7 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/dist/hooks/handler.d.ts +1 -3
- package/plugin/dist/hooks/handler.d.ts.map +1 -1
- package/plugin/dist/hooks/handler.js +22 -310
- package/plugin/dist/hooks/handler.js.map +1 -1
- package/plugin/dist/index.d.ts +1 -3
- package/plugin/dist/index.d.ts.map +1 -1
- package/plugin/dist/index.js +392 -1895
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/{observations-CorAAc1A.d.mts → observations-Ch0nc47i.d.mts} +1 -23
- package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
- package/plugin/dist/{tool-registry-e710BvXq.mjs → tool-registry-CZ3mJ4iR.mjs} +13 -932
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
- package/plugin/hooks/hooks.json +6 -6
- package/plugin/scripts/README.md +1 -19
- package/plugin/scripts/bump-version.sh +3 -1
- package/plugin/scripts/ensure-deps.sh +2 -5
- package/plugin/scripts/install.sh +39 -115
- package/plugin/scripts/local-install.sh +58 -93
- package/plugin/scripts/setup-tmpdir.sh +65 -0
- package/plugin/scripts/uninstall.sh +38 -76
- package/plugin/scripts/update.sh +69 -20
- package/plugin/scripts/verify-install.sh +25 -69
- package/plugin/ui/activity.js +0 -12
- package/plugin/ui/app.js +54 -24
- package/plugin/ui/graph.js +186 -413
- package/plugin/ui/help.js +172 -876
- package/plugin/ui/index.html +242 -506
- package/plugin/ui/settings.js +17 -781
- package/plugin/ui/styles.css +44 -990
- package/plugin/ui/timeline.js +2 -2
- package/plugin/CLAUDE.md +0 -10
- package/plugin/commands/recall.md +0 -55
- package/plugin/commands/remember.md +0 -34
- package/plugin/commands/resume.md +0 -45
- package/plugin/commands/stash.md +0 -34
- package/plugin/commands/status.md +0 -33
- package/plugin/dist/observations-CorAAc1A.d.mts.map +0 -1
- package/plugin/dist/tool-registry-e710BvXq.mjs.map +0 -1
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +0 -17
- package/plugin/scripts/dev-sync.sh +0 -58
- package/plugin/ui/help/activity-feed.png +0 -0
- package/plugin/ui/help/analysis-panel.png +0 -0
- package/plugin/ui/help/graph-toolbar.png +0 -0
- package/plugin/ui/help/graph-view.png +0 -0
- package/plugin/ui/help/settings.png +0 -0
- package/plugin/ui/help/timeline.png +0 -0
- package/plugin/ui/tools.js +0 -826
package/plugin/ui/graph.js
CHANGED
|
@@ -31,8 +31,17 @@ const ENTITY_STYLES = {
|
|
|
31
31
|
Reference: { color: '#f0883e', shape: 'hexagon' },
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
var
|
|
34
|
+
// Waypoint type colors for path overlay
|
|
35
|
+
var WAYPOINT_TYPE_COLORS = {
|
|
36
|
+
error: '#f85149',
|
|
37
|
+
attempt: '#d29922',
|
|
38
|
+
failure: '#f0883e',
|
|
39
|
+
success: '#3fb950',
|
|
40
|
+
pivot: '#a371f7',
|
|
41
|
+
revert: '#79c0ff',
|
|
42
|
+
discovery: '#58a6ff',
|
|
43
|
+
resolution: '#3fb950',
|
|
44
|
+
};
|
|
36
45
|
|
|
37
46
|
// Relationship type colors for edge coloring
|
|
38
47
|
var EDGE_TYPE_COLORS = {
|
|
@@ -188,17 +197,10 @@ var hiddenEdgeLabelTypes = new Set(
|
|
|
188
197
|
JSON.parse(localStorage.getItem('laminark-hidden-edge-types') || '[]')
|
|
189
198
|
);
|
|
190
199
|
|
|
191
|
-
//
|
|
192
|
-
var
|
|
193
|
-
var
|
|
194
|
-
var
|
|
195
|
-
var pathfinderInfoEl = null;
|
|
196
|
-
|
|
197
|
-
// Graph freeze state (suppresses SSE-driven refreshes)
|
|
198
|
-
var graphFrozen = false;
|
|
199
|
-
var graphStale = false;
|
|
200
|
-
var syncBtnEl = null;
|
|
201
|
-
var freezeBtnEl = null;
|
|
200
|
+
// Path overlay state
|
|
201
|
+
var pathOverlayGroup = null;
|
|
202
|
+
var pathOverlayVisible = localStorage.getItem('laminark-path-overlay') !== 'false';
|
|
203
|
+
var pathData = []; // Array of { id, status, triggerSummary, waypoints: [{id, type, summary, nodeId?}] }
|
|
202
204
|
|
|
203
205
|
// ---------------------------------------------------------------------------
|
|
204
206
|
// initGraph
|
|
@@ -250,6 +252,7 @@ function initGraph(containerId) {
|
|
|
250
252
|
// Layer groups in paint order (back to front)
|
|
251
253
|
edgesGroup = svgG.append('g').attr('class', 'edges-group');
|
|
252
254
|
edgeLabelsGroup = svgG.append('g').attr('class', 'edge-labels-group');
|
|
255
|
+
pathOverlayGroup = svgG.append('g').attr('class', 'path-overlay-group');
|
|
253
256
|
nodesGroup = svgG.append('g').attr('class', 'nodes-group');
|
|
254
257
|
nodeLabelsGroup = svgG.append('g').attr('class', 'node-labels-group');
|
|
255
258
|
|
|
@@ -260,16 +263,16 @@ function initGraph(containerId) {
|
|
|
260
263
|
svgG.attr('transform', event.transform);
|
|
261
264
|
currentZoom = event.transform.k;
|
|
262
265
|
updateLevelOfDetail();
|
|
266
|
+
renderPathOverlay();
|
|
263
267
|
});
|
|
264
268
|
svg.call(zoomBehavior);
|
|
265
269
|
|
|
266
|
-
// Background click: deselect + hide detail panel
|
|
270
|
+
// Background click: deselect + hide detail panel
|
|
267
271
|
svg.on('click', function (event) {
|
|
268
272
|
if (event.target === svg.node() || event.target.closest('.graph-zoom-group') === svgG.node() && !event.target.closest('.node-group')) {
|
|
269
273
|
hideDetailPanel();
|
|
270
274
|
selectedNodeId = null;
|
|
271
275
|
if (nodesGroup) nodesGroup.selectAll('.node-group').classed('selected', false);
|
|
272
|
-
clearPathfinder();
|
|
273
276
|
}
|
|
274
277
|
});
|
|
275
278
|
|
|
@@ -302,9 +305,7 @@ function initGraph(containerId) {
|
|
|
302
305
|
|
|
303
306
|
initContextMenu();
|
|
304
307
|
initEdgeLabelToggle();
|
|
305
|
-
|
|
306
|
-
initFreezeButton();
|
|
307
|
-
initSyncButton();
|
|
308
|
+
initPathOverlayToggle();
|
|
308
309
|
|
|
309
310
|
// Create tooltip element
|
|
310
311
|
tooltipEl = document.createElement('div');
|
|
@@ -353,11 +354,8 @@ function createSimulation() {
|
|
|
353
354
|
var width = containerEl ? containerEl.clientWidth : 800;
|
|
354
355
|
var height = containerEl ? containerEl.clientHeight : 600;
|
|
355
356
|
|
|
356
|
-
var simNodeIds = new Set(nodeData.filter(function (d) { return !d.hidden; }).map(function (d) { return d.id; }));
|
|
357
357
|
var visibleEdges = edgeData.filter(function (d) {
|
|
358
|
-
|
|
359
|
-
var tgtId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
360
|
-
return simNodeIds.has(srcId) && simNodeIds.has(tgtId);
|
|
358
|
+
return !d.source.hidden && !d.target.hidden;
|
|
361
359
|
});
|
|
362
360
|
|
|
363
361
|
// Degree-scaled repulsion: more links = stronger push away
|
|
@@ -597,12 +595,6 @@ function dragEnded(event, d) {
|
|
|
597
595
|
// ---------------------------------------------------------------------------
|
|
598
596
|
|
|
599
597
|
async function handleNodeClick(d) {
|
|
600
|
-
// Pathfinder mode: collect start/end nodes
|
|
601
|
-
if (pathfinderActive) {
|
|
602
|
-
handlePathfinderClick(d);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
598
|
selectedNodeId = d.id;
|
|
607
599
|
if (nodesGroup) {
|
|
608
600
|
nodesGroup.selectAll('.node-group').classed('selected', function (n) { return n.id === d.id; });
|
|
@@ -666,15 +658,6 @@ async function loadGraphData(filters) {
|
|
|
666
658
|
return { nodeCount: nodeData.length, edgeCount: edgeData.length };
|
|
667
659
|
}
|
|
668
660
|
|
|
669
|
-
// Don't reload while graph is frozen (manual pause or pathfinder result).
|
|
670
|
-
// Data will be fetched when the user unfreezes or clicks sync.
|
|
671
|
-
if (graphFrozen) {
|
|
672
|
-
console.log('[laminark:graph] Skipping loadGraphData (graph frozen)');
|
|
673
|
-
graphStale = true;
|
|
674
|
-
updateSyncButton();
|
|
675
|
-
return { nodeCount: nodeData.length, edgeCount: edgeData.length };
|
|
676
|
-
}
|
|
677
|
-
|
|
678
661
|
var data;
|
|
679
662
|
if (window.laminarkApp && window.laminarkApp.fetchGraphData) {
|
|
680
663
|
data = await window.laminarkApp.fetchGraphData(filters);
|
|
@@ -699,11 +682,11 @@ async function loadGraphData(filters) {
|
|
|
699
682
|
edgeData = [];
|
|
700
683
|
renderGraph();
|
|
701
684
|
updateGraphStats(0, 0);
|
|
702
|
-
|
|
685
|
+
showEmptyState();
|
|
703
686
|
return { nodeCount: 0, edgeCount: 0 };
|
|
704
687
|
}
|
|
705
688
|
|
|
706
|
-
|
|
689
|
+
hideEmptyState();
|
|
707
690
|
|
|
708
691
|
// Build data arrays
|
|
709
692
|
nodeData = data.nodes.map(function (node) {
|
|
@@ -744,6 +727,11 @@ async function loadGraphData(filters) {
|
|
|
744
727
|
setTimeout(function () { fitToView(); }, 800);
|
|
745
728
|
}
|
|
746
729
|
|
|
730
|
+
// Load path overlay after graph data
|
|
731
|
+
if (pathOverlayVisible) {
|
|
732
|
+
setTimeout(function () { loadPathOverlay(); }, 1000);
|
|
733
|
+
}
|
|
734
|
+
|
|
747
735
|
var counts = { nodeCount: data.nodes.length, edgeCount: data.edges.length };
|
|
748
736
|
updateGraphStats(counts.nodeCount, counts.edgeCount);
|
|
749
737
|
console.log('[laminark:graph] Loaded', counts.nodeCount, 'nodes,', counts.edgeCount, 'edges');
|
|
@@ -769,7 +757,7 @@ function addNode(nodeDataIn) {
|
|
|
769
757
|
createdAt: nodeDataIn.createdAt,
|
|
770
758
|
hidden: false,
|
|
771
759
|
});
|
|
772
|
-
|
|
760
|
+
hideEmptyState();
|
|
773
761
|
}
|
|
774
762
|
|
|
775
763
|
renderGraph();
|
|
@@ -816,7 +804,7 @@ function removeElements(ids) {
|
|
|
816
804
|
renderGraph();
|
|
817
805
|
updateGraphStatsFromData();
|
|
818
806
|
|
|
819
|
-
if (nodeData.length === 0)
|
|
807
|
+
if (nodeData.length === 0) showEmptyState();
|
|
820
808
|
}
|
|
821
809
|
|
|
822
810
|
// ---------------------------------------------------------------------------
|
|
@@ -979,7 +967,7 @@ function updateFilterCounts() {
|
|
|
979
967
|
// Empty state
|
|
980
968
|
// ---------------------------------------------------------------------------
|
|
981
969
|
|
|
982
|
-
function
|
|
970
|
+
function showEmptyState() {
|
|
983
971
|
if (!containerEl) return;
|
|
984
972
|
var existing = containerEl.querySelector('.graph-empty-state');
|
|
985
973
|
if (existing) { existing.style.display = ''; return; }
|
|
@@ -990,7 +978,7 @@ function showGraphEmptyState() {
|
|
|
990
978
|
containerEl.appendChild(msg);
|
|
991
979
|
}
|
|
992
980
|
|
|
993
|
-
function
|
|
981
|
+
function hideEmptyState() {
|
|
994
982
|
if (!containerEl) return;
|
|
995
983
|
var existing = containerEl.querySelector('.graph-empty-state');
|
|
996
984
|
if (existing) existing.style.display = 'none';
|
|
@@ -1191,13 +1179,9 @@ function initEdgeLabelToggle() {
|
|
|
1191
1179
|
dropdown.appendChild(row);
|
|
1192
1180
|
});
|
|
1193
1181
|
|
|
1194
|
-
// Insert dropdown after button
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
btnWrapper.style.position = 'relative';
|
|
1198
|
-
btn.parentElement.insertBefore(btnWrapper, btn);
|
|
1199
|
-
btnWrapper.appendChild(btn);
|
|
1200
|
-
btnWrapper.appendChild(dropdown);
|
|
1182
|
+
// Insert dropdown after button
|
|
1183
|
+
btn.parentElement.style.position = 'relative';
|
|
1184
|
+
btn.insertAdjacentElement('afterend', dropdown);
|
|
1201
1185
|
|
|
1202
1186
|
// Toggle dropdown on click
|
|
1203
1187
|
btn.addEventListener('click', function (e) {
|
|
@@ -1423,12 +1407,6 @@ function queueBatchUpdate(update) {
|
|
|
1423
1407
|
batchFlushTimer = setTimeout(flushBatchUpdates, BATCH_DELAY_MS);
|
|
1424
1408
|
}
|
|
1425
1409
|
|
|
1426
|
-
function clearBatchQueue() {
|
|
1427
|
-
if (batchFlushTimer) clearTimeout(batchFlushTimer);
|
|
1428
|
-
batchFlushTimer = null;
|
|
1429
|
-
batchQueue = [];
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
1410
|
function flushBatchUpdates() {
|
|
1433
1411
|
if (!svg || batchQueue.length === 0) return;
|
|
1434
1412
|
|
|
@@ -1472,14 +1450,7 @@ function flushBatchUpdates() {
|
|
|
1472
1450
|
batchFlushTimer = null;
|
|
1473
1451
|
|
|
1474
1452
|
if (newNodes > 0 || newEdges > 0) {
|
|
1475
|
-
|
|
1476
|
-
if (graphFrozen) {
|
|
1477
|
-
console.log('[laminark:graph] Batch data accumulated but render suppressed (graph frozen)');
|
|
1478
|
-
graphStale = true;
|
|
1479
|
-
updateSyncButton();
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
hideGraphEmptyState();
|
|
1453
|
+
hideEmptyState();
|
|
1483
1454
|
renderGraph();
|
|
1484
1455
|
if (!isStaticLayout && simulation) simulation.alpha(0.3).restart();
|
|
1485
1456
|
console.log('[laminark:graph] Batch update: added ' + newNodes + ' nodes, ' + newEdges + ' edges');
|
|
@@ -1641,7 +1612,7 @@ function updateBreadcrumbs() {
|
|
|
1641
1612
|
// ---------------------------------------------------------------------------
|
|
1642
1613
|
|
|
1643
1614
|
function setLayout(layoutName) {
|
|
1644
|
-
var validLayouts = ['clustered', 'hierarchical', 'concentric', 'communities'
|
|
1615
|
+
var validLayouts = ['clustered', 'hierarchical', 'concentric', 'communities'];
|
|
1645
1616
|
if (validLayouts.indexOf(layoutName) === -1) return;
|
|
1646
1617
|
|
|
1647
1618
|
var previousLayout = currentLayout;
|
|
@@ -1664,8 +1635,6 @@ function setLayout(layoutName) {
|
|
|
1664
1635
|
applyHierarchicalLayout();
|
|
1665
1636
|
} else if (layoutName === 'concentric') {
|
|
1666
1637
|
applyConcentricLayout();
|
|
1667
|
-
} else if (layoutName === 'detangle') {
|
|
1668
|
-
applyDetangleLayout();
|
|
1669
1638
|
} else {
|
|
1670
1639
|
applyClusteredLayout();
|
|
1671
1640
|
}
|
|
@@ -1877,119 +1846,6 @@ function applyCommunitiesLayout() {
|
|
|
1877
1846
|
});
|
|
1878
1847
|
}
|
|
1879
1848
|
|
|
1880
|
-
// ---------------------------------------------------------------------------
|
|
1881
|
-
// Detangle layout — minimizes edge crossings via custom force
|
|
1882
|
-
// ---------------------------------------------------------------------------
|
|
1883
|
-
|
|
1884
|
-
/**
|
|
1885
|
-
* Tests whether line segment (p1→p2) crosses (p3→p4).
|
|
1886
|
-
* Returns true if the segments properly intersect.
|
|
1887
|
-
*/
|
|
1888
|
-
function segmentsIntersect(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) {
|
|
1889
|
-
var d1x = p2x - p1x, d1y = p2y - p1y;
|
|
1890
|
-
var d2x = p4x - p3x, d2y = p4y - p3y;
|
|
1891
|
-
var cross = d1x * d2y - d1y * d2x;
|
|
1892
|
-
if (Math.abs(cross) < 1e-10) return false;
|
|
1893
|
-
var t = ((p3x - p1x) * d2y - (p3y - p1y) * d2x) / cross;
|
|
1894
|
-
var u = ((p3x - p1x) * d1y - (p3y - p1y) * d1x) / cross;
|
|
1895
|
-
return t > 0.01 && t < 0.99 && u > 0.01 && u < 0.99;
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
/**
|
|
1899
|
-
* Custom D3 force that detects edge crossings and pushes nodes apart
|
|
1900
|
-
* to untangle the graph. Runs O(E^2) per tick — fine for <1000 edges.
|
|
1901
|
-
*/
|
|
1902
|
-
function forceUncross(edges) {
|
|
1903
|
-
var strength = 8;
|
|
1904
|
-
var nodes;
|
|
1905
|
-
|
|
1906
|
-
function force(alpha) {
|
|
1907
|
-
if (!nodes || edges.length < 2) return;
|
|
1908
|
-
var effectiveStrength = strength * alpha;
|
|
1909
|
-
|
|
1910
|
-
for (var i = 0; i < edges.length; i++) {
|
|
1911
|
-
var e1 = edges[i];
|
|
1912
|
-
var s1 = e1.source, t1 = e1.target;
|
|
1913
|
-
if (!s1 || !t1 || s1.x == null || t1.x == null) continue;
|
|
1914
|
-
|
|
1915
|
-
for (var j = i + 1; j < edges.length; j++) {
|
|
1916
|
-
var e2 = edges[j];
|
|
1917
|
-
var s2 = e2.source, t2 = e2.target;
|
|
1918
|
-
if (!s2 || !t2 || s2.x == null || t2.x == null) continue;
|
|
1919
|
-
|
|
1920
|
-
// Skip if edges share a node
|
|
1921
|
-
if (s1 === s2 || s1 === t2 || t1 === s2 || t1 === t2) continue;
|
|
1922
|
-
|
|
1923
|
-
if (segmentsIntersect(s1.x, s1.y, t1.x, t1.y, s2.x, s2.y, t2.x, t2.y)) {
|
|
1924
|
-
// Push the midpoints of the two edges apart
|
|
1925
|
-
var m1x = (s1.x + t1.x) / 2, m1y = (s1.y + t1.y) / 2;
|
|
1926
|
-
var m2x = (s2.x + t2.x) / 2, m2y = (s2.y + t2.y) / 2;
|
|
1927
|
-
var dx = m1x - m2x, dy = m1y - m2y;
|
|
1928
|
-
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
1929
|
-
var push = effectiveStrength / dist;
|
|
1930
|
-
var fx = dx * push, fy = dy * push;
|
|
1931
|
-
|
|
1932
|
-
// Apply to non-fixed nodes of edge 1 (push away from edge 2)
|
|
1933
|
-
if (s1.fx == null) { s1.vx += fx * 0.5; s1.vy += fy * 0.5; }
|
|
1934
|
-
if (t1.fx == null) { t1.vx += fx * 0.5; t1.vy += fy * 0.5; }
|
|
1935
|
-
// Apply to non-fixed nodes of edge 2 (push opposite direction)
|
|
1936
|
-
if (s2.fx == null) { s2.vx -= fx * 0.5; s2.vy -= fy * 0.5; }
|
|
1937
|
-
if (t2.fx == null) { t2.vx -= fx * 0.5; t2.vy -= fy * 0.5; }
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
force.initialize = function (_nodes) { nodes = _nodes; };
|
|
1944
|
-
force.strength = function (s) { if (!arguments.length) return strength; strength = s; return force; };
|
|
1945
|
-
|
|
1946
|
-
return force;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
function applyDetangleLayout() {
|
|
1950
|
-
isStaticLayout = false;
|
|
1951
|
-
nodeData.forEach(function (d) { d.fx = null; d.fy = null; });
|
|
1952
|
-
|
|
1953
|
-
// Let renderGraph create the standard simulation first (it calls createSimulation)
|
|
1954
|
-
renderGraph();
|
|
1955
|
-
|
|
1956
|
-
// Now enhance the existing simulation with the uncross force
|
|
1957
|
-
if (simulation) {
|
|
1958
|
-
var simNodeIds = new Set(nodeData.filter(function (d) { return !d.hidden; }).map(function (d) { return d.id; }));
|
|
1959
|
-
var visibleEdges = edgeData.filter(function (d) {
|
|
1960
|
-
var srcId = typeof d.source === 'object' ? d.source.id : d.source;
|
|
1961
|
-
var tgtId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
1962
|
-
return simNodeIds.has(srcId) && simNodeIds.has(tgtId);
|
|
1963
|
-
});
|
|
1964
|
-
|
|
1965
|
-
var width = containerEl ? containerEl.clientWidth : 800;
|
|
1966
|
-
var height = containerEl ? containerEl.clientHeight : 600;
|
|
1967
|
-
|
|
1968
|
-
// Widen link distances and strengthen repulsion for more spacing
|
|
1969
|
-
simulation
|
|
1970
|
-
.force('link', d3.forceLink(visibleEdges)
|
|
1971
|
-
.id(function (d) { return d.id; })
|
|
1972
|
-
.distance(function (d) {
|
|
1973
|
-
var srcDeg = (typeof d.source === 'object' ? d.source._degree : 0) || 0;
|
|
1974
|
-
var tgtDeg = (typeof d.target === 'object' ? d.target._degree : 0) || 0;
|
|
1975
|
-
return 140 + Math.sqrt(srcDeg + tgtDeg) * 25;
|
|
1976
|
-
})
|
|
1977
|
-
.strength(0.8))
|
|
1978
|
-
.force('collide', d3.forceCollide().radius(function (d) {
|
|
1979
|
-
return getNodeSize(d) + 20;
|
|
1980
|
-
}).strength(1))
|
|
1981
|
-
.force('uncross', forceUncross(visibleEdges).strength(12))
|
|
1982
|
-
.force('x', d3.forceX(width / 2).strength(0.02))
|
|
1983
|
-
.force('y', d3.forceY(height / 2).strength(0.02))
|
|
1984
|
-
.alphaDecay(0.01)
|
|
1985
|
-
.velocityDecay(0.4)
|
|
1986
|
-
.alpha(1)
|
|
1987
|
-
.restart();
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
setTimeout(function () { fitToView(); }, 1500);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
1849
|
function initLayoutSelector() {
|
|
1994
1850
|
var btns = document.querySelectorAll('.layout-btn');
|
|
1995
1851
|
btns.forEach(function (btn) {
|
|
@@ -2242,265 +2098,182 @@ if (document.readyState === 'loading') {
|
|
|
2242
2098
|
}
|
|
2243
2099
|
|
|
2244
2100
|
// ---------------------------------------------------------------------------
|
|
2245
|
-
//
|
|
2101
|
+
// Path overlay
|
|
2246
2102
|
// ---------------------------------------------------------------------------
|
|
2247
2103
|
|
|
2248
|
-
function
|
|
2249
|
-
|
|
2250
|
-
var graphArea = containerEl ? containerEl.parentElement : null;
|
|
2251
|
-
var toolbar = graphArea ? graphArea.querySelector('.graph-toolbar') : null;
|
|
2252
|
-
if (!toolbar) return;
|
|
2253
|
-
|
|
2254
|
-
freezeBtnEl = document.createElement('button');
|
|
2255
|
-
freezeBtnEl.className = 'graph-freeze-btn';
|
|
2256
|
-
freezeBtnEl.title = 'Pause live updates';
|
|
2257
|
-
freezeBtnEl.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/></svg>';
|
|
2258
|
-
freezeBtnEl.addEventListener('click', function () {
|
|
2259
|
-
toggleGraphFrozen();
|
|
2260
|
-
});
|
|
2261
|
-
// Insert before the fit button (last in toolbar)
|
|
2262
|
-
var fitBtn = toolbar.querySelector('.fit-btn');
|
|
2263
|
-
if (fitBtn) {
|
|
2264
|
-
toolbar.insertBefore(freezeBtnEl, fitBtn);
|
|
2265
|
-
} else {
|
|
2266
|
-
toolbar.appendChild(freezeBtnEl);
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2104
|
+
async function loadPathOverlay() {
|
|
2105
|
+
if (!svg || !pathOverlayVisible) return;
|
|
2269
2106
|
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2107
|
+
try {
|
|
2108
|
+
var params = new URLSearchParams();
|
|
2109
|
+
if (window.laminarkState && window.laminarkState.currentProject) {
|
|
2110
|
+
params.set('project', window.laminarkState.currentProject);
|
|
2111
|
+
}
|
|
2112
|
+
params.set('limit', '10');
|
|
2113
|
+
var url = '/api/paths' + (params.toString() ? '?' + params.toString() : '');
|
|
2114
|
+
var res = await fetch(url);
|
|
2115
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2116
|
+
var data = await res.json();
|
|
2117
|
+
pathData = (data.paths || []).filter(function(p) { return p.status === 'active' || p.status === 'resolved'; });
|
|
2118
|
+
|
|
2119
|
+
// For each path, fetch waypoints
|
|
2120
|
+
for (var i = 0; i < pathData.length; i++) {
|
|
2121
|
+
try {
|
|
2122
|
+
var detailRes = await fetch('/api/paths/' + encodeURIComponent(pathData[i].id));
|
|
2123
|
+
if (detailRes.ok) {
|
|
2124
|
+
var detail = await detailRes.json();
|
|
2125
|
+
pathData[i].waypoints = detail.waypoints || [];
|
|
2126
|
+
}
|
|
2127
|
+
} catch (e) { pathData[i].waypoints = []; }
|
|
2128
|
+
}
|
|
2273
2129
|
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
clearPathfinder();
|
|
2278
|
-
loadGraphData();
|
|
2279
|
-
updateSyncButton();
|
|
2130
|
+
renderPathOverlay();
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
console.error('[laminark:graph] Failed to load path overlay:', err);
|
|
2280
2133
|
}
|
|
2281
2134
|
}
|
|
2282
2135
|
|
|
2283
|
-
function
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
function updateFreezeButton() {
|
|
2289
|
-
if (!freezeBtnEl) return;
|
|
2290
|
-
freezeBtnEl.classList.toggle('active', graphFrozen);
|
|
2291
|
-
freezeBtnEl.title = graphFrozen ? 'Resume live updates' : 'Pause live updates';
|
|
2292
|
-
// Swap icon: pause when live, play when frozen
|
|
2293
|
-
if (graphFrozen) {
|
|
2294
|
-
freezeBtnEl.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2l10 6-10 6V2z"/></svg>';
|
|
2295
|
-
} else {
|
|
2296
|
-
freezeBtnEl.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/></svg>';
|
|
2136
|
+
function renderPathOverlay() {
|
|
2137
|
+
if (!pathOverlayGroup || !pathOverlayVisible) {
|
|
2138
|
+
if (pathOverlayGroup) pathOverlayGroup.style('display', 'none');
|
|
2139
|
+
return;
|
|
2297
2140
|
}
|
|
2298
|
-
|
|
2141
|
+
pathOverlayGroup.style('display', null);
|
|
2142
|
+
pathOverlayGroup.selectAll('*').remove();
|
|
2299
2143
|
|
|
2300
|
-
|
|
2301
|
-
if (!containerEl) return;
|
|
2302
|
-
syncBtnEl = document.createElement('button');
|
|
2303
|
-
syncBtnEl.className = 'graph-sync-btn hidden';
|
|
2304
|
-
syncBtnEl.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M1.5 8a6.5 6.5 0 0 1 11.25-4.4l-1.54 1.54A.5.5 0 0 0 11.56 6H15.5a.5.5 0 0 0 .5-.5V1.56a.5.5 0 0 0-.85-.35L13.6 2.76A8 8 0 0 0 0 8h1.5zm13 0a6.5 6.5 0 0 1-11.25 4.4l1.54-1.54A.5.5 0 0 0 4.44 10H.5a.5.5 0 0 0-.5.5v3.94a.5.5 0 0 0 .85.35l1.55-1.55A8 8 0 0 0 16 8h-1.5z"/></svg> Sync';
|
|
2305
|
-
syncBtnEl.addEventListener('click', function () {
|
|
2306
|
-
graphStale = false;
|
|
2307
|
-
graphFrozen = false;
|
|
2308
|
-
updateFreezeButton();
|
|
2309
|
-
clearPathfinder();
|
|
2310
|
-
loadGraphData();
|
|
2311
|
-
updateSyncButton();
|
|
2312
|
-
});
|
|
2313
|
-
containerEl.appendChild(syncBtnEl);
|
|
2314
|
-
}
|
|
2144
|
+
if (pathData.length === 0) return;
|
|
2315
2145
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
if (graphStale && graphFrozen) {
|
|
2319
|
-
syncBtnEl.classList.remove('hidden');
|
|
2320
|
-
syncBtnEl.classList.add('stale');
|
|
2321
|
-
} else {
|
|
2322
|
-
syncBtnEl.classList.add('hidden');
|
|
2323
|
-
syncBtnEl.classList.remove('stale');
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2146
|
+
var width = containerEl ? containerEl.clientWidth : 800;
|
|
2147
|
+
var height = containerEl ? containerEl.clientHeight : 600;
|
|
2326
2148
|
|
|
2327
|
-
//
|
|
2328
|
-
|
|
2329
|
-
// ---------------------------------------------------------------------------
|
|
2149
|
+
// Get current transform to position in screen space
|
|
2150
|
+
var transform = d3.zoomTransform(svg.node());
|
|
2330
2151
|
|
|
2331
|
-
function
|
|
2332
|
-
|
|
2333
|
-
if (!btn) return;
|
|
2152
|
+
pathData.forEach(function(path, pathIndex) {
|
|
2153
|
+
if (!path.waypoints || path.waypoints.length === 0) return;
|
|
2334
2154
|
|
|
2335
|
-
|
|
2155
|
+
var pathGroup = pathOverlayGroup.append('g')
|
|
2156
|
+
.attr('class', 'path-trail')
|
|
2157
|
+
.attr('data-path-id', path.id);
|
|
2336
2158
|
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
}
|
|
2159
|
+
// Position waypoints evenly spaced along a line
|
|
2160
|
+
var waypoints = path.waypoints;
|
|
2161
|
+
var margin = 80;
|
|
2162
|
+
var yBase = (height - 60) / transform.k - transform.y / transform.k;
|
|
2163
|
+
var xStart = (-transform.x / transform.k) + margin / transform.k;
|
|
2164
|
+
var xEnd = (-transform.x / transform.k) + (width - margin) / transform.k;
|
|
2165
|
+
var spacing = waypoints.length > 1 ? (xEnd - xStart) / (waypoints.length - 1) : 0;
|
|
2345
2166
|
|
|
2346
|
-
function
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
pathfinderSelection = [d.id];
|
|
2350
|
-
pathfinderResult = null;
|
|
2351
|
-
clearPathfinderVisuals();
|
|
2352
|
-
if (nodesGroup) {
|
|
2353
|
-
nodesGroup.selectAll('.node-group')
|
|
2354
|
-
.classed('pathfinder-start', function (n) { return n.id === d.id; })
|
|
2355
|
-
.classed('pathfinder-end', false);
|
|
2356
|
-
}
|
|
2357
|
-
updatePathfinderInfo('Select target node...');
|
|
2358
|
-
} else if (pathfinderSelection.length === 1) {
|
|
2359
|
-
if (pathfinderSelection[0] === d.id) return; // Same node
|
|
2360
|
-
pathfinderSelection.push(d.id);
|
|
2361
|
-
|
|
2362
|
-
// Mark end node
|
|
2363
|
-
if (nodesGroup) {
|
|
2364
|
-
nodesGroup.selectAll('.node-group')
|
|
2365
|
-
.classed('pathfinder-end', function (n) { return n.id === d.id; });
|
|
2366
|
-
}
|
|
2167
|
+
var points = waypoints.map(function(wp, i) {
|
|
2168
|
+
return { x: xStart + i * spacing, y: yBase + pathIndex * 40 / transform.k };
|
|
2169
|
+
});
|
|
2367
2170
|
|
|
2368
|
-
//
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2171
|
+
// Draw connecting line (animated dashed)
|
|
2172
|
+
if (points.length > 1) {
|
|
2173
|
+
var lineGen = d3.line()
|
|
2174
|
+
.x(function(d) { return d.x; })
|
|
2175
|
+
.y(function(d) { return d.y; })
|
|
2176
|
+
.curve(d3.curveCatmullRom.alpha(0.5));
|
|
2177
|
+
|
|
2178
|
+
pathGroup.append('path')
|
|
2179
|
+
.attr('class', 'path-line' + (path.status === 'active' ? ' path-line-active' : ''))
|
|
2180
|
+
.attr('d', lineGen(points))
|
|
2181
|
+
.attr('fill', 'none')
|
|
2182
|
+
.attr('stroke', path.status === 'resolved' ? '#3fb950' : '#d29922')
|
|
2183
|
+
.attr('stroke-width', 2.5 / transform.k)
|
|
2184
|
+
.attr('stroke-dasharray', (6 / transform.k) + ' ' + (4 / transform.k))
|
|
2185
|
+
.attr('opacity', 0.8);
|
|
2377
2186
|
}
|
|
2378
|
-
} else {
|
|
2379
|
-
// Already have 2 — restart with this node as new start
|
|
2380
|
-
pathfinderSelection = [d.id];
|
|
2381
|
-
pathfinderResult = null;
|
|
2382
|
-
clearPathfinderVisuals();
|
|
2383
|
-
if (nodesGroup) {
|
|
2384
|
-
nodesGroup.selectAll('.node-group')
|
|
2385
|
-
.classed('pathfinder-start', function (n) { return n.id === d.id; })
|
|
2386
|
-
.classed('pathfinder-end', false);
|
|
2387
|
-
}
|
|
2388
|
-
updatePathfinderInfo('Select target node...');
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
2187
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2188
|
+
// Draw waypoint markers
|
|
2189
|
+
points.forEach(function(pt, i) {
|
|
2190
|
+
var wp = waypoints[i];
|
|
2191
|
+
var color = WAYPOINT_TYPE_COLORS[wp.waypoint_type] || '#8b949e';
|
|
2192
|
+
var radius = 6 / transform.k;
|
|
2193
|
+
|
|
2194
|
+
var marker = pathGroup.append('g')
|
|
2195
|
+
.attr('class', 'waypoint-marker')
|
|
2196
|
+
.attr('transform', 'translate(' + pt.x + ',' + pt.y + ')')
|
|
2197
|
+
.style('cursor', 'pointer');
|
|
2198
|
+
|
|
2199
|
+
marker.append('circle')
|
|
2200
|
+
.attr('r', radius)
|
|
2201
|
+
.attr('fill', color)
|
|
2202
|
+
.attr('stroke', '#0d1117')
|
|
2203
|
+
.attr('stroke-width', 1.5 / transform.k);
|
|
2204
|
+
|
|
2205
|
+
// Sequence number
|
|
2206
|
+
marker.append('text')
|
|
2207
|
+
.attr('text-anchor', 'middle')
|
|
2208
|
+
.attr('dominant-baseline', 'central')
|
|
2209
|
+
.attr('font-size', (8 / transform.k) + 'px')
|
|
2210
|
+
.attr('fill', '#fff')
|
|
2211
|
+
.attr('font-weight', '700')
|
|
2212
|
+
.attr('pointer-events', 'none')
|
|
2213
|
+
.text(wp.sequence_order);
|
|
2214
|
+
|
|
2215
|
+
// Tooltip on hover
|
|
2216
|
+
marker.append('title')
|
|
2217
|
+
.text(wp.waypoint_type + ': ' + (wp.summary || '').substring(0, 80));
|
|
2218
|
+
|
|
2219
|
+
// Click to show path detail
|
|
2220
|
+
marker.on('click', function(event) {
|
|
2221
|
+
event.stopPropagation();
|
|
2222
|
+
if (window.laminarkApp && window.laminarkApp.fetchPathDetail) {
|
|
2223
|
+
window.laminarkApp.fetchPathDetail(path.id).then(function(detail) {
|
|
2224
|
+
if (detail) {
|
|
2225
|
+
document.dispatchEvent(new CustomEvent('laminark:show_path_detail', { detail: detail }));
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2422
2228
|
}
|
|
2423
2229
|
});
|
|
2424
|
-
|
|
2425
|
-
}
|
|
2230
|
+
});
|
|
2426
2231
|
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
var
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2232
|
+
// Path label
|
|
2233
|
+
if (points.length > 0) {
|
|
2234
|
+
var labelX = points[0].x;
|
|
2235
|
+
var labelY = points[0].y - 12 / transform.k;
|
|
2236
|
+
pathGroup.append('text')
|
|
2237
|
+
.attr('class', 'path-label')
|
|
2238
|
+
.attr('x', labelX)
|
|
2239
|
+
.attr('y', labelY)
|
|
2240
|
+
.attr('font-size', (10 / transform.k) + 'px')
|
|
2241
|
+
.attr('fill', path.status === 'resolved' ? '#3fb950' : '#d29922')
|
|
2242
|
+
.attr('opacity', 0.9)
|
|
2243
|
+
.text((path.trigger_summary || 'Debug Path').substring(0, 40));
|
|
2437
2244
|
}
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
return null; // No path found
|
|
2245
|
+
});
|
|
2441
2246
|
}
|
|
2442
2247
|
|
|
2443
|
-
function
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
// Dim non-path nodes
|
|
2447
|
-
nodesGroup.selectAll('.node-group')
|
|
2448
|
-
.classed('search-dimmed', function (d) { return !result.nodeIds.has(d.id); })
|
|
2449
|
-
.classed('pathfinder-start', function (d) { return d.id === pathfinderSelection[0]; })
|
|
2450
|
-
.classed('pathfinder-end', function (d) { return d.id === pathfinderSelection[1]; });
|
|
2451
|
-
|
|
2452
|
-
nodeLabelsGroup.selectAll('.node-label')
|
|
2453
|
-
.classed('search-dimmed', function (d) { return !result.nodeIds.has(d.id); });
|
|
2454
|
-
|
|
2455
|
-
// Dim non-path edges, highlight path edges
|
|
2456
|
-
edgesGroup.selectAll('.edge')
|
|
2457
|
-
.classed('search-dimmed', function (d) { return !result.edgeIds.has(d.id); })
|
|
2458
|
-
.classed('path-highlight', function (d) { return result.edgeIds.has(d.id); });
|
|
2459
|
-
|
|
2460
|
-
edgeLabelsGroup.selectAll('.edge-label')
|
|
2461
|
-
.classed('search-dimmed', function (d) { return !result.edgeIds.has(d.id); });
|
|
2462
|
-
|
|
2463
|
-
// Show info
|
|
2464
|
-
var hops = result.path.length - 1;
|
|
2465
|
-
var types = result.relTypes.join(' > ');
|
|
2466
|
-
updatePathfinderInfo(hops + ' hop' + (hops !== 1 ? 's' : '') + ': ' + types);
|
|
2248
|
+
function addPathOverlay(pathEvent) {
|
|
2249
|
+
loadPathOverlay();
|
|
2467
2250
|
}
|
|
2468
2251
|
|
|
2469
|
-
function
|
|
2470
|
-
|
|
2471
|
-
pathfinderResult = null;
|
|
2472
|
-
clearPathfinderVisuals();
|
|
2473
|
-
hidePathfinderInfo();
|
|
2252
|
+
function updatePathOverlay(waypointEvent) {
|
|
2253
|
+
loadPathOverlay();
|
|
2474
2254
|
}
|
|
2475
2255
|
|
|
2476
|
-
function
|
|
2477
|
-
|
|
2478
|
-
nodesGroup.selectAll('.node-group')
|
|
2479
|
-
.classed('pathfinder-start', false)
|
|
2480
|
-
.classed('pathfinder-end', false)
|
|
2481
|
-
.classed('search-dimmed', false);
|
|
2482
|
-
nodeLabelsGroup.selectAll('.node-label')
|
|
2483
|
-
.classed('search-dimmed', false);
|
|
2484
|
-
edgesGroup.selectAll('.edge')
|
|
2485
|
-
.classed('search-dimmed', false)
|
|
2486
|
-
.classed('path-highlight', false);
|
|
2487
|
-
edgeLabelsGroup.selectAll('.edge-label')
|
|
2488
|
-
.classed('search-dimmed', false);
|
|
2256
|
+
function resolvePathOverlay(resolveEvent) {
|
|
2257
|
+
loadPathOverlay();
|
|
2489
2258
|
}
|
|
2490
2259
|
|
|
2491
|
-
function
|
|
2492
|
-
|
|
2493
|
-
if (!
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
containerEl.appendChild(pathfinderInfoEl);
|
|
2497
|
-
}
|
|
2498
|
-
pathfinderInfoEl.textContent = text;
|
|
2499
|
-
pathfinderInfoEl.style.display = '';
|
|
2500
|
-
}
|
|
2260
|
+
function initPathOverlayToggle() {
|
|
2261
|
+
var btn = document.getElementById('paths-toggle-btn');
|
|
2262
|
+
if (!btn) return;
|
|
2263
|
+
|
|
2264
|
+
btn.classList.toggle('active', pathOverlayVisible);
|
|
2501
2265
|
|
|
2502
|
-
function
|
|
2503
|
-
|
|
2266
|
+
btn.addEventListener('click', function() {
|
|
2267
|
+
pathOverlayVisible = !pathOverlayVisible;
|
|
2268
|
+
localStorage.setItem('laminark-path-overlay', pathOverlayVisible ? 'true' : 'false');
|
|
2269
|
+
btn.classList.toggle('active', pathOverlayVisible);
|
|
2270
|
+
|
|
2271
|
+
if (pathOverlayVisible) {
|
|
2272
|
+
loadPathOverlay();
|
|
2273
|
+
} else {
|
|
2274
|
+
if (pathOverlayGroup) pathOverlayGroup.style('display', 'none');
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2504
2277
|
}
|
|
2505
2278
|
|
|
2506
2279
|
// ---------------------------------------------------------------------------
|
|
@@ -2524,7 +2297,6 @@ window.laminarkGraph = {
|
|
|
2524
2297
|
hideDetailPanel: hideDetailPanel,
|
|
2525
2298
|
selectAndCenterNode: selectAndCenterNode,
|
|
2526
2299
|
queueBatchUpdate: queueBatchUpdate,
|
|
2527
|
-
clearBatchQueue: clearBatchQueue,
|
|
2528
2300
|
togglePerfOverlay: togglePerfOverlay,
|
|
2529
2301
|
enterFocusMode: enterFocusMode,
|
|
2530
2302
|
exitFocusMode: exitFocusMode,
|
|
@@ -2540,10 +2312,11 @@ window.laminarkGraph = {
|
|
|
2540
2312
|
clearCommunityColors: clearCommunityColors,
|
|
2541
2313
|
showLinkedNodesOfType: showLinkedNodesOfType,
|
|
2542
2314
|
hideContextMenu: hideContextMenu,
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2315
|
+
addPathOverlay: addPathOverlay,
|
|
2316
|
+
updatePathOverlay: updatePathOverlay,
|
|
2317
|
+
resolvePathOverlay: resolvePathOverlay,
|
|
2318
|
+
loadPathOverlay: loadPathOverlay,
|
|
2319
|
+
isPathOverlayVisible: function() { return pathOverlayVisible; },
|
|
2547
2320
|
toggleEdgeLabels: function (type) {
|
|
2548
2321
|
if (type) {
|
|
2549
2322
|
if (hiddenEdgeLabelTypes.has(type)) hiddenEdgeLabelTypes.delete(type);
|