laminark 2.21.7 → 2.21.9
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 +36 -71
- package/package.json +9 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +10 -0
- package/plugin/commands/recall.md +55 -0
- package/plugin/commands/remember.md +34 -0
- package/plugin/commands/resume.md +45 -0
- package/plugin/commands/stash.md +34 -0
- package/plugin/commands/status.md +33 -0
- package/plugin/dist/hooks/handler.d.ts +3 -1
- package/plugin/dist/hooks/handler.d.ts.map +1 -1
- package/plugin/dist/hooks/handler.js +312 -23
- package/plugin/dist/hooks/handler.js.map +1 -1
- package/plugin/dist/index.d.ts +3 -1
- package/plugin/dist/index.d.ts.map +1 -1
- package/plugin/dist/index.js +2111 -525
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/{observations-Ch0nc47i.d.mts → observations-CorAAc1A.d.mts} +23 -1
- package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
- package/plugin/dist/{tool-registry-CZ3mJ4iR.mjs → tool-registry-D8un_AcG.mjs} +932 -13
- package/plugin/dist/tool-registry-D8un_AcG.mjs.map +1 -0
- package/plugin/hooks/hooks.json +6 -6
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +17 -0
- package/plugin/scripts/README.md +19 -1
- package/plugin/scripts/bump-version.sh +24 -19
- package/plugin/scripts/dev-sync.sh +58 -0
- package/plugin/scripts/ensure-deps.sh +5 -2
- package/plugin/scripts/install.sh +115 -39
- package/plugin/scripts/local-install.sh +93 -58
- package/plugin/scripts/uninstall.sh +76 -38
- package/plugin/scripts/update.sh +20 -69
- package/plugin/scripts/verify-install.sh +69 -25
- package/plugin/ui/activity.js +12 -0
- package/plugin/ui/app.js +24 -54
- package/plugin/ui/graph.js +413 -186
- 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/help.js +876 -172
- package/plugin/ui/index.html +506 -242
- package/plugin/ui/settings.js +781 -17
- package/plugin/ui/styles.css +990 -44
- package/plugin/ui/timeline.js +2 -2
- package/plugin/ui/tools.js +826 -0
- package/.claude-plugin/marketplace.json +0 -15
- package/plugin/dist/observations-Ch0nc47i.d.mts.map +0 -1
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +0 -1
- package/plugin/scripts/setup-tmpdir.sh +0 -65
package/plugin/ui/graph.js
CHANGED
|
@@ -31,17 +31,8 @@ const ENTITY_STYLES = {
|
|
|
31
31
|
Reference: { color: '#f0883e', shape: 'hexagon' },
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
var
|
|
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
|
-
};
|
|
34
|
+
// Pathfinder highlight color
|
|
35
|
+
var PATHFINDER_COLOR = '#f0883e';
|
|
45
36
|
|
|
46
37
|
// Relationship type colors for edge coloring
|
|
47
38
|
var EDGE_TYPE_COLORS = {
|
|
@@ -197,10 +188,17 @@ var hiddenEdgeLabelTypes = new Set(
|
|
|
197
188
|
JSON.parse(localStorage.getItem('laminark-hidden-edge-types') || '[]')
|
|
198
189
|
);
|
|
199
190
|
|
|
200
|
-
//
|
|
201
|
-
var
|
|
202
|
-
var
|
|
203
|
-
var
|
|
191
|
+
// Pathfinder state
|
|
192
|
+
var pathfinderActive = false;
|
|
193
|
+
var pathfinderSelection = []; // Max 2 node IDs: [startId, endId]
|
|
194
|
+
var pathfinderResult = null; // { nodeIds: Set, edgeIds: Set, path: [{nodeId, edgeId}] }
|
|
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;
|
|
204
202
|
|
|
205
203
|
// ---------------------------------------------------------------------------
|
|
206
204
|
// initGraph
|
|
@@ -252,7 +250,6 @@ function initGraph(containerId) {
|
|
|
252
250
|
// Layer groups in paint order (back to front)
|
|
253
251
|
edgesGroup = svgG.append('g').attr('class', 'edges-group');
|
|
254
252
|
edgeLabelsGroup = svgG.append('g').attr('class', 'edge-labels-group');
|
|
255
|
-
pathOverlayGroup = svgG.append('g').attr('class', 'path-overlay-group');
|
|
256
253
|
nodesGroup = svgG.append('g').attr('class', 'nodes-group');
|
|
257
254
|
nodeLabelsGroup = svgG.append('g').attr('class', 'node-labels-group');
|
|
258
255
|
|
|
@@ -263,16 +260,16 @@ function initGraph(containerId) {
|
|
|
263
260
|
svgG.attr('transform', event.transform);
|
|
264
261
|
currentZoom = event.transform.k;
|
|
265
262
|
updateLevelOfDetail();
|
|
266
|
-
renderPathOverlay();
|
|
267
263
|
});
|
|
268
264
|
svg.call(zoomBehavior);
|
|
269
265
|
|
|
270
|
-
// Background click: deselect + hide detail panel
|
|
266
|
+
// Background click: deselect + hide detail panel + clear pathfinder
|
|
271
267
|
svg.on('click', function (event) {
|
|
272
268
|
if (event.target === svg.node() || event.target.closest('.graph-zoom-group') === svgG.node() && !event.target.closest('.node-group')) {
|
|
273
269
|
hideDetailPanel();
|
|
274
270
|
selectedNodeId = null;
|
|
275
271
|
if (nodesGroup) nodesGroup.selectAll('.node-group').classed('selected', false);
|
|
272
|
+
clearPathfinder();
|
|
276
273
|
}
|
|
277
274
|
});
|
|
278
275
|
|
|
@@ -305,7 +302,9 @@ function initGraph(containerId) {
|
|
|
305
302
|
|
|
306
303
|
initContextMenu();
|
|
307
304
|
initEdgeLabelToggle();
|
|
308
|
-
|
|
305
|
+
initPathfinderToggle();
|
|
306
|
+
initFreezeButton();
|
|
307
|
+
initSyncButton();
|
|
309
308
|
|
|
310
309
|
// Create tooltip element
|
|
311
310
|
tooltipEl = document.createElement('div');
|
|
@@ -354,8 +353,11 @@ function createSimulation() {
|
|
|
354
353
|
var width = containerEl ? containerEl.clientWidth : 800;
|
|
355
354
|
var height = containerEl ? containerEl.clientHeight : 600;
|
|
356
355
|
|
|
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
|
-
|
|
358
|
+
var srcId = typeof d.source === 'object' ? d.source.id : d.source;
|
|
359
|
+
var tgtId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
360
|
+
return simNodeIds.has(srcId) && simNodeIds.has(tgtId);
|
|
359
361
|
});
|
|
360
362
|
|
|
361
363
|
// Degree-scaled repulsion: more links = stronger push away
|
|
@@ -595,6 +597,12 @@ function dragEnded(event, d) {
|
|
|
595
597
|
// ---------------------------------------------------------------------------
|
|
596
598
|
|
|
597
599
|
async function handleNodeClick(d) {
|
|
600
|
+
// Pathfinder mode: collect start/end nodes
|
|
601
|
+
if (pathfinderActive) {
|
|
602
|
+
handlePathfinderClick(d);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
598
606
|
selectedNodeId = d.id;
|
|
599
607
|
if (nodesGroup) {
|
|
600
608
|
nodesGroup.selectAll('.node-group').classed('selected', function (n) { return n.id === d.id; });
|
|
@@ -658,6 +666,15 @@ async function loadGraphData(filters) {
|
|
|
658
666
|
return { nodeCount: nodeData.length, edgeCount: edgeData.length };
|
|
659
667
|
}
|
|
660
668
|
|
|
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
|
+
|
|
661
678
|
var data;
|
|
662
679
|
if (window.laminarkApp && window.laminarkApp.fetchGraphData) {
|
|
663
680
|
data = await window.laminarkApp.fetchGraphData(filters);
|
|
@@ -682,11 +699,11 @@ async function loadGraphData(filters) {
|
|
|
682
699
|
edgeData = [];
|
|
683
700
|
renderGraph();
|
|
684
701
|
updateGraphStats(0, 0);
|
|
685
|
-
|
|
702
|
+
showGraphEmptyState();
|
|
686
703
|
return { nodeCount: 0, edgeCount: 0 };
|
|
687
704
|
}
|
|
688
705
|
|
|
689
|
-
|
|
706
|
+
hideGraphEmptyState();
|
|
690
707
|
|
|
691
708
|
// Build data arrays
|
|
692
709
|
nodeData = data.nodes.map(function (node) {
|
|
@@ -727,11 +744,6 @@ async function loadGraphData(filters) {
|
|
|
727
744
|
setTimeout(function () { fitToView(); }, 800);
|
|
728
745
|
}
|
|
729
746
|
|
|
730
|
-
// Load path overlay after graph data
|
|
731
|
-
if (pathOverlayVisible) {
|
|
732
|
-
setTimeout(function () { loadPathOverlay(); }, 1000);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
747
|
var counts = { nodeCount: data.nodes.length, edgeCount: data.edges.length };
|
|
736
748
|
updateGraphStats(counts.nodeCount, counts.edgeCount);
|
|
737
749
|
console.log('[laminark:graph] Loaded', counts.nodeCount, 'nodes,', counts.edgeCount, 'edges');
|
|
@@ -757,7 +769,7 @@ function addNode(nodeDataIn) {
|
|
|
757
769
|
createdAt: nodeDataIn.createdAt,
|
|
758
770
|
hidden: false,
|
|
759
771
|
});
|
|
760
|
-
|
|
772
|
+
hideGraphEmptyState();
|
|
761
773
|
}
|
|
762
774
|
|
|
763
775
|
renderGraph();
|
|
@@ -804,7 +816,7 @@ function removeElements(ids) {
|
|
|
804
816
|
renderGraph();
|
|
805
817
|
updateGraphStatsFromData();
|
|
806
818
|
|
|
807
|
-
if (nodeData.length === 0)
|
|
819
|
+
if (nodeData.length === 0) showGraphEmptyState();
|
|
808
820
|
}
|
|
809
821
|
|
|
810
822
|
// ---------------------------------------------------------------------------
|
|
@@ -967,7 +979,7 @@ function updateFilterCounts() {
|
|
|
967
979
|
// Empty state
|
|
968
980
|
// ---------------------------------------------------------------------------
|
|
969
981
|
|
|
970
|
-
function
|
|
982
|
+
function showGraphEmptyState() {
|
|
971
983
|
if (!containerEl) return;
|
|
972
984
|
var existing = containerEl.querySelector('.graph-empty-state');
|
|
973
985
|
if (existing) { existing.style.display = ''; return; }
|
|
@@ -978,7 +990,7 @@ function showEmptyState() {
|
|
|
978
990
|
containerEl.appendChild(msg);
|
|
979
991
|
}
|
|
980
992
|
|
|
981
|
-
function
|
|
993
|
+
function hideGraphEmptyState() {
|
|
982
994
|
if (!containerEl) return;
|
|
983
995
|
var existing = containerEl.querySelector('.graph-empty-state');
|
|
984
996
|
if (existing) existing.style.display = 'none';
|
|
@@ -1179,9 +1191,13 @@ function initEdgeLabelToggle() {
|
|
|
1179
1191
|
dropdown.appendChild(row);
|
|
1180
1192
|
});
|
|
1181
1193
|
|
|
1182
|
-
// Insert dropdown after button
|
|
1183
|
-
|
|
1184
|
-
|
|
1194
|
+
// Insert dropdown after button — wrap btn in a relative container
|
|
1195
|
+
// so the dropdown positions correctly without breaking the toolbar's absolute positioning
|
|
1196
|
+
var btnWrapper = document.createElement('div');
|
|
1197
|
+
btnWrapper.style.position = 'relative';
|
|
1198
|
+
btn.parentElement.insertBefore(btnWrapper, btn);
|
|
1199
|
+
btnWrapper.appendChild(btn);
|
|
1200
|
+
btnWrapper.appendChild(dropdown);
|
|
1185
1201
|
|
|
1186
1202
|
// Toggle dropdown on click
|
|
1187
1203
|
btn.addEventListener('click', function (e) {
|
|
@@ -1407,6 +1423,12 @@ function queueBatchUpdate(update) {
|
|
|
1407
1423
|
batchFlushTimer = setTimeout(flushBatchUpdates, BATCH_DELAY_MS);
|
|
1408
1424
|
}
|
|
1409
1425
|
|
|
1426
|
+
function clearBatchQueue() {
|
|
1427
|
+
if (batchFlushTimer) clearTimeout(batchFlushTimer);
|
|
1428
|
+
batchFlushTimer = null;
|
|
1429
|
+
batchQueue = [];
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1410
1432
|
function flushBatchUpdates() {
|
|
1411
1433
|
if (!svg || batchQueue.length === 0) return;
|
|
1412
1434
|
|
|
@@ -1450,7 +1472,14 @@ function flushBatchUpdates() {
|
|
|
1450
1472
|
batchFlushTimer = null;
|
|
1451
1473
|
|
|
1452
1474
|
if (newNodes > 0 || newEdges > 0) {
|
|
1453
|
-
|
|
1475
|
+
// Suppress rendering while graph is frozen
|
|
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();
|
|
1454
1483
|
renderGraph();
|
|
1455
1484
|
if (!isStaticLayout && simulation) simulation.alpha(0.3).restart();
|
|
1456
1485
|
console.log('[laminark:graph] Batch update: added ' + newNodes + ' nodes, ' + newEdges + ' edges');
|
|
@@ -1612,7 +1641,7 @@ function updateBreadcrumbs() {
|
|
|
1612
1641
|
// ---------------------------------------------------------------------------
|
|
1613
1642
|
|
|
1614
1643
|
function setLayout(layoutName) {
|
|
1615
|
-
var validLayouts = ['clustered', 'hierarchical', 'concentric', 'communities'];
|
|
1644
|
+
var validLayouts = ['clustered', 'hierarchical', 'concentric', 'communities', 'detangle'];
|
|
1616
1645
|
if (validLayouts.indexOf(layoutName) === -1) return;
|
|
1617
1646
|
|
|
1618
1647
|
var previousLayout = currentLayout;
|
|
@@ -1635,6 +1664,8 @@ function setLayout(layoutName) {
|
|
|
1635
1664
|
applyHierarchicalLayout();
|
|
1636
1665
|
} else if (layoutName === 'concentric') {
|
|
1637
1666
|
applyConcentricLayout();
|
|
1667
|
+
} else if (layoutName === 'detangle') {
|
|
1668
|
+
applyDetangleLayout();
|
|
1638
1669
|
} else {
|
|
1639
1670
|
applyClusteredLayout();
|
|
1640
1671
|
}
|
|
@@ -1846,6 +1877,119 @@ function applyCommunitiesLayout() {
|
|
|
1846
1877
|
});
|
|
1847
1878
|
}
|
|
1848
1879
|
|
|
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
|
+
|
|
1849
1993
|
function initLayoutSelector() {
|
|
1850
1994
|
var btns = document.querySelectorAll('.layout-btn');
|
|
1851
1995
|
btns.forEach(function (btn) {
|
|
@@ -2098,182 +2242,265 @@ if (document.readyState === 'loading') {
|
|
|
2098
2242
|
}
|
|
2099
2243
|
|
|
2100
2244
|
// ---------------------------------------------------------------------------
|
|
2101
|
-
//
|
|
2245
|
+
// Freeze button (pauses SSE-driven refreshes) + Sync button (reload stale data)
|
|
2102
2246
|
// ---------------------------------------------------------------------------
|
|
2103
2247
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2248
|
+
function initFreezeButton() {
|
|
2249
|
+
// Add freeze toggle to the toolbar (toolbar is sibling of containerEl, not inside it)
|
|
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
|
+
}
|
|
2106
2269
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
}
|
|
2270
|
+
function toggleGraphFrozen() {
|
|
2271
|
+
graphFrozen = !graphFrozen;
|
|
2272
|
+
updateFreezeButton();
|
|
2129
2273
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2274
|
+
if (!graphFrozen && graphStale) {
|
|
2275
|
+
// Unfreeze: reload pending data
|
|
2276
|
+
graphStale = false;
|
|
2277
|
+
clearPathfinder();
|
|
2278
|
+
loadGraphData();
|
|
2279
|
+
updateSyncButton();
|
|
2133
2280
|
}
|
|
2134
2281
|
}
|
|
2135
2282
|
|
|
2136
|
-
function
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2283
|
+
function setGraphFrozen(frozen) {
|
|
2284
|
+
graphFrozen = frozen;
|
|
2285
|
+
updateFreezeButton();
|
|
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>';
|
|
2140
2297
|
}
|
|
2141
|
-
|
|
2142
|
-
pathOverlayGroup.selectAll('*').remove();
|
|
2298
|
+
}
|
|
2143
2299
|
|
|
2144
|
-
|
|
2300
|
+
function initSyncButton() {
|
|
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
|
+
}
|
|
2145
2315
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2316
|
+
function updateSyncButton() {
|
|
2317
|
+
if (!syncBtnEl) return;
|
|
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
|
+
}
|
|
2148
2326
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2327
|
+
// ---------------------------------------------------------------------------
|
|
2328
|
+
// Pathfinder: shortest path between two nodes
|
|
2329
|
+
// ---------------------------------------------------------------------------
|
|
2151
2330
|
|
|
2152
|
-
|
|
2153
|
-
|
|
2331
|
+
function initPathfinderToggle() {
|
|
2332
|
+
var btn = document.getElementById('paths-toggle-btn');
|
|
2333
|
+
if (!btn) return;
|
|
2154
2334
|
|
|
2155
|
-
|
|
2156
|
-
.attr('class', 'path-trail')
|
|
2157
|
-
.attr('data-path-id', path.id);
|
|
2335
|
+
btn.classList.toggle('active', pathfinderActive);
|
|
2158
2336
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2337
|
+
btn.addEventListener('click', function () {
|
|
2338
|
+
pathfinderActive = !pathfinderActive;
|
|
2339
|
+
btn.classList.toggle('active', pathfinderActive);
|
|
2340
|
+
if (!pathfinderActive) {
|
|
2341
|
+
clearPathfinder();
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2166
2345
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2346
|
+
function handlePathfinderClick(d) {
|
|
2347
|
+
if (pathfinderSelection.length === 0) {
|
|
2348
|
+
// First node selected
|
|
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
|
+
}
|
|
2170
2367
|
|
|
2171
|
-
//
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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);
|
|
2368
|
+
// Run BFS
|
|
2369
|
+
var result = bfsShortestPath(pathfinderSelection[0], pathfinderSelection[1]);
|
|
2370
|
+
if (result) {
|
|
2371
|
+
pathfinderResult = result;
|
|
2372
|
+
visualizePathfinderResult(result);
|
|
2373
|
+
// Auto-freeze to protect the path visualization
|
|
2374
|
+
setGraphFrozen(true);
|
|
2375
|
+
} else {
|
|
2376
|
+
updatePathfinderInfo('No path found between these nodes');
|
|
2186
2377
|
}
|
|
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
|
+
}
|
|
2187
2391
|
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
.
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
});
|
|
2392
|
+
function bfsShortestPath(startId, endId) {
|
|
2393
|
+
// Build adjacency from edgeData (treat as undirected)
|
|
2394
|
+
var adj = {};
|
|
2395
|
+
edgeData.forEach(function (e) {
|
|
2396
|
+
var srcId = typeof e.source === 'object' ? e.source.id : e.source;
|
|
2397
|
+
var tgtId = typeof e.target === 'object' ? e.target.id : e.target;
|
|
2398
|
+
if (!adj[srcId]) adj[srcId] = [];
|
|
2399
|
+
if (!adj[tgtId]) adj[tgtId] = [];
|
|
2400
|
+
adj[srcId].push({ nodeId: tgtId, edgeId: e.id });
|
|
2401
|
+
adj[tgtId].push({ nodeId: srcId, edgeId: e.id });
|
|
2402
|
+
});
|
|
2403
|
+
|
|
2404
|
+
// BFS
|
|
2405
|
+
var visited = new Set();
|
|
2406
|
+
visited.add(startId);
|
|
2407
|
+
var queue = [{ nodeId: startId, path: [{ nodeId: startId, edgeId: null }] }];
|
|
2408
|
+
|
|
2409
|
+
while (queue.length > 0) {
|
|
2410
|
+
var current = queue.shift();
|
|
2411
|
+
if (current.nodeId === endId) {
|
|
2412
|
+
// Build result sets
|
|
2413
|
+
var nodeIds = new Set();
|
|
2414
|
+
var edgeIds = new Set();
|
|
2415
|
+
var relTypes = [];
|
|
2416
|
+
current.path.forEach(function (step) {
|
|
2417
|
+
nodeIds.add(step.nodeId);
|
|
2418
|
+
if (step.edgeId) {
|
|
2419
|
+
edgeIds.add(step.edgeId);
|
|
2420
|
+
var edge = edgeData.find(function (e) { return e.id === step.edgeId; });
|
|
2421
|
+
if (edge) relTypes.push(edge.type);
|
|
2228
2422
|
}
|
|
2229
2423
|
});
|
|
2230
|
-
|
|
2424
|
+
return { nodeIds: nodeIds, edgeIds: edgeIds, path: current.path, relTypes: relTypes };
|
|
2425
|
+
}
|
|
2231
2426
|
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
var
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
.
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
.attr('opacity', 0.9)
|
|
2243
|
-
.text((path.trigger_summary || 'Debug Path').substring(0, 40));
|
|
2427
|
+
var neighbors = adj[current.nodeId] || [];
|
|
2428
|
+
for (var i = 0; i < neighbors.length; i++) {
|
|
2429
|
+
var nb = neighbors[i];
|
|
2430
|
+
if (!visited.has(nb.nodeId)) {
|
|
2431
|
+
visited.add(nb.nodeId);
|
|
2432
|
+
queue.push({
|
|
2433
|
+
nodeId: nb.nodeId,
|
|
2434
|
+
path: current.path.concat([{ nodeId: nb.nodeId, edgeId: nb.edgeId }]),
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2244
2437
|
}
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2438
|
+
}
|
|
2247
2439
|
|
|
2248
|
-
|
|
2249
|
-
loadPathOverlay();
|
|
2440
|
+
return null; // No path found
|
|
2250
2441
|
}
|
|
2251
2442
|
|
|
2252
|
-
function
|
|
2253
|
-
|
|
2254
|
-
|
|
2443
|
+
function visualizePathfinderResult(result) {
|
|
2444
|
+
if (!nodesGroup || !edgesGroup) return;
|
|
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); });
|
|
2255
2459
|
|
|
2256
|
-
|
|
2257
|
-
|
|
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);
|
|
2258
2467
|
}
|
|
2259
2468
|
|
|
2260
|
-
function
|
|
2261
|
-
|
|
2262
|
-
|
|
2469
|
+
function clearPathfinder() {
|
|
2470
|
+
pathfinderSelection = [];
|
|
2471
|
+
pathfinderResult = null;
|
|
2472
|
+
clearPathfinderVisuals();
|
|
2473
|
+
hidePathfinderInfo();
|
|
2474
|
+
}
|
|
2263
2475
|
|
|
2264
|
-
|
|
2476
|
+
function clearPathfinderVisuals() {
|
|
2477
|
+
if (!nodesGroup) return;
|
|
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);
|
|
2489
|
+
}
|
|
2265
2490
|
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2491
|
+
function updatePathfinderInfo(text) {
|
|
2492
|
+
if (!containerEl) return;
|
|
2493
|
+
if (!pathfinderInfoEl) {
|
|
2494
|
+
pathfinderInfoEl = document.createElement('div');
|
|
2495
|
+
pathfinderInfoEl.className = 'pathfinder-info';
|
|
2496
|
+
containerEl.appendChild(pathfinderInfoEl);
|
|
2497
|
+
}
|
|
2498
|
+
pathfinderInfoEl.textContent = text;
|
|
2499
|
+
pathfinderInfoEl.style.display = '';
|
|
2500
|
+
}
|
|
2270
2501
|
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
} else {
|
|
2274
|
-
if (pathOverlayGroup) pathOverlayGroup.style('display', 'none');
|
|
2275
|
-
}
|
|
2276
|
-
});
|
|
2502
|
+
function hidePathfinderInfo() {
|
|
2503
|
+
if (pathfinderInfoEl) pathfinderInfoEl.style.display = 'none';
|
|
2277
2504
|
}
|
|
2278
2505
|
|
|
2279
2506
|
// ---------------------------------------------------------------------------
|
|
@@ -2297,6 +2524,7 @@ window.laminarkGraph = {
|
|
|
2297
2524
|
hideDetailPanel: hideDetailPanel,
|
|
2298
2525
|
selectAndCenterNode: selectAndCenterNode,
|
|
2299
2526
|
queueBatchUpdate: queueBatchUpdate,
|
|
2527
|
+
clearBatchQueue: clearBatchQueue,
|
|
2300
2528
|
togglePerfOverlay: togglePerfOverlay,
|
|
2301
2529
|
enterFocusMode: enterFocusMode,
|
|
2302
2530
|
exitFocusMode: exitFocusMode,
|
|
@@ -2312,11 +2540,10 @@ window.laminarkGraph = {
|
|
|
2312
2540
|
clearCommunityColors: clearCommunityColors,
|
|
2313
2541
|
showLinkedNodesOfType: showLinkedNodesOfType,
|
|
2314
2542
|
hideContextMenu: hideContextMenu,
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
isPathOverlayVisible: function() { return pathOverlayVisible; },
|
|
2543
|
+
clearPathfinder: clearPathfinder,
|
|
2544
|
+
isPathfinderActive: function () { return pathfinderActive; },
|
|
2545
|
+
isGraphFrozen: function () { return graphFrozen; },
|
|
2546
|
+
toggleGraphFrozen: toggleGraphFrozen,
|
|
2320
2547
|
toggleEdgeLabels: function (type) {
|
|
2321
2548
|
if (type) {
|
|
2322
2549
|
if (hiddenEdgeLabelTypes.has(type)) hiddenEdgeLabelTypes.delete(type);
|