ui: add graph render toggle to pause heavy graph rendering

This commit is contained in:
Youzini-afk
2026-04-13 01:04:57 +08:00
parent 7bda266b9c
commit a1a570f637
4 changed files with 158 additions and 15 deletions

View File

@@ -220,6 +220,7 @@ export class GraphRenderer {
this._suppressMouseUntil = 0;
this.animId = null;
this.enabled = true;
// Callbacks
this.onNodeSelect = isLegacy ? null : (options?.onNodeSelect || null);
@@ -242,7 +243,6 @@ export class GraphRenderer {
*/
loadGraph(graph, layoutHints = {}) {
const prevSelectedId = this.selectedNode?.id || null;
this.nodeMap.clear();
this._lastGraph = graph;
this._lastLayoutHints = layoutHints && typeof layoutHints === 'object'
? { ...layoutHints }
@@ -253,6 +253,12 @@ export class GraphRenderer {
);
}
if (!this.enabled) {
return;
}
this.nodeMap.clear();
const dpr = window.devicePixelRatio || 1;
const W = this.canvas.width / dpr;
const H = this.canvas.height / dpr;
@@ -306,7 +312,7 @@ export class GraphRenderer {
setTheme(themeName) {
this.themeName = themeName;
this.colors = getNodeColors(themeName);
this._render();
if (this.enabled) this._render();
}
/**
@@ -314,7 +320,45 @@ export class GraphRenderer {
*/
highlightNode(nodeId) {
this.selectedNode = this.nodeMap.get(nodeId) || null;
this._render();
if (this.enabled) this._render();
}
_clearCanvas() {
const ctx = this.ctx;
if (!ctx) return;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.canvas.style.cursor = 'default';
}
setEnabled(enabled = true) {
const nextEnabled = enabled !== false;
if (this.enabled === nextEnabled) {
if (!nextEnabled) this._clearCanvas();
return;
}
this.enabled = nextEnabled;
this._cancelAnim();
this.dragNode = null;
this.isDragging = false;
this.isPanning = false;
this._touchSession = null;
this._dragStartMouse = null;
this.hoveredNode = null;
if (!nextEnabled) {
this.nodeMap.clear();
this.nodes = [];
this.edges = [];
this._regionPanels = [];
this._clearCanvas();
return;
}
this.canvas.style.cursor = 'grab';
if (this._lastGraph) {
this.loadGraph(this._lastGraph, this._lastLayoutHints);
} else {
this._render();
}
}
// ==================== 分区布局 ====================
@@ -676,6 +720,10 @@ export class GraphRenderer {
}
_render() {
if (!this.enabled) {
this._clearCanvas();
return;
}
const ctx = this.ctx;
const dpr = window.devicePixelRatio || 1;
const W = this.canvas.width / dpr;
@@ -752,7 +800,7 @@ export class GraphRenderer {
}
_scheduleRender() {
if (this.animId) return;
if (!this.enabled || this.animId) return;
this.animId = requestAnimationFrame(() => {
this.animId = null;
this._render();
@@ -814,13 +862,10 @@ export class GraphRenderer {
}
}
/** @deprecated 力导向动画已移除;保留空实现以兼容旧调用 */
stopAnimation() {
this._cancelAnim();
}
// ==================== 交互 ====================
_bindEvents() {
const c = this.canvas;
@@ -830,8 +875,8 @@ export class GraphRenderer {
c.addEventListener('wheel', (e) => this._onWheel(e), { passive: false });
c.addEventListener('dblclick', (e) => this._onDoubleClick(e));
// 触摸:单指始终平移画布,松手时在未移动过的情况下视为「点击」选中节点(避免拖动画布时误拖节点)
c.addEventListener('touchstart', (e) => {
if (!this.enabled) return;
if (e.touches.length !== 1) {
this._touchSession = null;
return;
@@ -854,7 +899,7 @@ export class GraphRenderer {
};
}, { passive: false });
c.addEventListener('touchmove', (e) => {
if (!this._touchSession || e.touches.length !== 1) return;
if (!this.enabled || !this._touchSession || e.touches.length !== 1) return;
e.preventDefault();
this._markTouchInteraction();
const t = e.touches[0];
@@ -871,8 +916,8 @@ export class GraphRenderer {
this._touchSession.lastY = t.clientY;
this._scheduleRender();
}, { passive: false });
c.addEventListener('touchend', (e) => {
if (!this._touchSession) return;
c.addEventListener('touchend', () => {
if (!this.enabled || !this._touchSession) return;
this._markTouchInteraction();
const sess = this._touchSession;
this._touchSession = null;
@@ -888,6 +933,7 @@ export class GraphRenderer {
}
});
c.addEventListener('touchcancel', () => {
if (!this.enabled) return;
this._markTouchInteraction();
this._touchSession = null;
this.dragNode = null;
@@ -902,7 +948,7 @@ export class GraphRenderer {
}
_shouldIgnoreMouseEvent() {
return Date.now() < this._suppressMouseUntil;
return !this.enabled || Date.now() < this._suppressMouseUntil;
}
_canvasToWorld(clientX, clientY) {
@@ -988,6 +1034,7 @@ export class GraphRenderer {
}
_onWheel(e) {
if (!this.enabled) return;
e.preventDefault();
const factor = e.deltaY > 0 ? 0.9 : 1.1;
const newScale = Math.max(0.2, Math.min(5, this.scale * factor));
@@ -1017,16 +1064,19 @@ export class GraphRenderer {
// ==================== 工具 ====================
zoomIn() {
if (!this.enabled) return;
this.scale = Math.min(5, this.scale * 1.2);
this._render();
}
zoomOut() {
if (!this.enabled) return;
this.scale = Math.max(0.2, this.scale * 0.8);
this._render();
}
resetView() {
if (!this.enabled) return;
this.scale = 1;
this.offsetX = 0;
this.offsetY = 0;
@@ -1060,6 +1110,11 @@ export class GraphRenderer {
this.canvas.style.width = w + 'px';
this.canvas.style.height = h + 'px';
if (!this.enabled) {
this._clearCanvas();
return;
}
if (this.nodes.length > 0 && this._regionPanels.length > 0) {
this._rebuildLayoutForCurrentViewport(w, h);
this._render();

View File

@@ -516,6 +516,9 @@
</div>
</div>
<div class="bme-mobile-graph-float-controls">
<button id="bme-mobile-render-toggle" title="暂停图谱渲染" type="button">
<i class="fa-solid fa-pause"></i>
</button>
<button id="bme-mobile-zoom-in" title="放大" type="button">
<i class="fa-solid fa-plus"></i>
</button>
@@ -610,6 +613,9 @@
</button>
</div>
<div class="bme-graph-controls">
<button id="bme-graph-render-toggle" title="暂停图谱渲染" type="button">
<i class="fa-solid fa-pause"></i>
</button>
<button id="bme-graph-zoom-in" title="放大" type="button">
<i class="fa-solid fa-plus"></i>
</button>

View File

@@ -346,6 +346,7 @@ let pendingVisibleGraphRefreshToken = "";
let pendingVisibleGraphRefreshForce = false;
let lastVisibleGraphRefreshToken = "";
let lastVisibleGraphRefreshAt = 0;
let graphRenderingEnabled = true;
// 由 index.js 注入的引用
let _getGraph = null;
@@ -732,6 +733,49 @@ function _clearScheduledVisibleGraphRefresh() {
pendingVisibleGraphRefreshForce = false;
}
function _isGraphRenderingEnabled() {
return graphRenderingEnabled !== false;
}
function _refreshGraphRenderToggleUi() {
const enabled = _isGraphRenderingEnabled();
const syncButton = (button) => {
if (!button) return;
const title = enabled ? "暂停图谱渲染" : "恢复图谱渲染";
button.classList.toggle("is-paused", !enabled);
button.classList.toggle("is-active", enabled);
button.title = title;
button.setAttribute("aria-label", title);
button.setAttribute("aria-pressed", enabled ? "true" : "false");
const icon = button.querySelector("i");
if (icon) {
icon.className = enabled ? "fa-solid fa-pause" : "fa-solid fa-play";
}
};
syncButton(document.getElementById("bme-graph-render-toggle"));
syncButton(document.getElementById("bme-mobile-render-toggle"));
}
function _applyGraphRenderEnabledState({ forceRefresh = false } = {}) {
const enabled = _isGraphRenderingEnabled();
graphRenderer?.setEnabled?.(enabled);
mobileGraphRenderer?.setEnabled?.(enabled);
_refreshGraphRenderToggleUi();
if (!enabled) {
_clearScheduledVisibleGraphRefresh();
return;
}
if (forceRefresh) {
_scheduleVisibleGraphWorkspaceRefresh({ force: true });
}
}
function _toggleGraphRenderingEnabled() {
graphRenderingEnabled = !_isGraphRenderingEnabled();
_applyGraphRenderEnabledState({ forceRefresh: graphRenderingEnabled });
_refreshGraphAvailabilityState();
}
function _refreshVisibleGraphWorkspace({ force = false } = {}) {
const visibleMode = _getVisibleGraphWorkspaceMode();
if (visibleMode === "hidden") {
@@ -1127,6 +1171,8 @@ export function openPanel() {
mobileGraphRenderer.onNodeSelect = (node) => _showNodeDetail(node);
}
_applyGraphRenderEnabledState();
const activeTabId =
panelEl?.querySelector(".bme-tab-btn.active")?.dataset.tab || currentTabId;
_switchTab(activeTabId);
@@ -3870,6 +3916,9 @@ function _getActiveGraphRenderer() {
}
function _bindGraphControls() {
document
.getElementById("bme-graph-render-toggle")
?.addEventListener("click", () => _toggleGraphRenderingEnabled());
document
.getElementById("bme-graph-zoom-in")
?.addEventListener("click", () => _getActiveGraphRenderer()?.zoomIn());
@@ -5251,6 +5300,9 @@ function _bindActions() {
});
// 移动端图谱浮动控件
document.getElementById("bme-mobile-render-toggle")?.addEventListener("click", () => {
_toggleGraphRenderingEnabled();
});
document.getElementById("bme-mobile-zoom-in")?.addEventListener("click", () => {
const r = _getActiveGraphRenderer?.();
r?.zoomIn?.();
@@ -11017,6 +11069,8 @@ function _refreshGraphAvailabilityState() {
const mobileOverlayText = document.getElementById("bme-mobile-graph-overlay-text");
const blocked = _isGraphWriteBlocked(loadInfo);
const loadLabel = _getGraphLoadLabel(loadInfo.loadState);
const pausedLabel = "图谱渲染已暂停,可点击工具栏按钮恢复。";
const renderingPaused = !_isGraphRenderingEnabled();
GRAPH_WRITE_ACTION_IDS.forEach((id) => {
const button = document.getElementById(id);
@@ -11025,6 +11079,7 @@ function _refreshGraphAvailabilityState() {
button.classList.toggle("is-runtime-disabled", blocked);
button.title = blocked ? loadLabel : "";
});
_refreshGraphRenderToggleUi();
if (banner) {
const shouldShowBanner = blocked;
@@ -11032,26 +11087,33 @@ function _refreshGraphAvailabilityState() {
banner.textContent = shouldShowBanner ? loadLabel : "";
}
const shouldShowOverlay =
const shouldShowRuntimeOverlay =
blocked ||
loadInfo.syncState === "syncing" ||
loadInfo.loadState === "loading" ||
loadInfo.loadState === "shadow-restored" ||
loadInfo.loadState === "blocked";
const shouldShowOverlay = shouldShowRuntimeOverlay || renderingPaused;
const overlayLabel = shouldShowRuntimeOverlay
? loadLabel
: renderingPaused
? pausedLabel
: "";
if (graphOverlay) {
graphOverlay.hidden = !shouldShowOverlay;
graphOverlay.classList.toggle("active", shouldShowOverlay);
}
if (graphOverlayText) {
graphOverlayText.textContent = shouldShowOverlay ? loadLabel : "";
graphOverlayText.textContent = overlayLabel;
}
if (mobileOverlay) {
mobileOverlay.hidden = !shouldShowOverlay;
mobileOverlay.classList.toggle("active", shouldShowOverlay);
}
if (mobileOverlayText) {
mobileOverlayText.textContent = shouldShowOverlay ? loadLabel : "";
mobileOverlayText.textContent = overlayLabel;
}
}