diff --git a/manifest.json b/manifest.json index 3245531..aba9c8b 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.9.5", + "version": "4.9.7", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } diff --git a/style.css b/style.css index 2882889..1e248b8 100644 --- a/style.css +++ b/style.css @@ -1257,6 +1257,12 @@ flex: 0 0 auto; } +.bme-memory-list-filters .bme-floor-input { + max-width: 120px; + flex: 0 0 auto; + font-variant-numeric: tabular-nums; +} + .bme-memory-list-scroll { flex: 1; overflow-y: auto; @@ -1676,6 +1682,16 @@ color: var(--bme-primary); } +.bme-graph-controls button.is-paused { + border-color: rgba(255, 181, 71, 0.55); + color: #ffb347; +} + +.bme-graph-controls button.is-active { + border-color: rgba(87, 199, 255, 0.55); + color: #57c7ff; +} + #bme-graph-canvas, #bme-mobile-graph-canvas, #bme-fullscreen-graph-canvas { @@ -5039,6 +5055,16 @@ color: var(--bme-primary); } +.bme-mobile-graph-float-controls button.is-paused { + border-color: rgba(255, 181, 71, 0.62); + color: #ffb347; +} + +.bme-mobile-graph-float-controls button.is-active { + border-color: rgba(87, 199, 255, 0.62); + color: #57c7ff; +} + /* Graph pane layout */ #bme-pane-graph { display: none; diff --git a/ui/graph-renderer.js b/ui/graph-renderer.js index 05e0d1e..abdfee3 100644 --- a/ui/graph-renderer.js +++ b/ui/graph-renderer.js @@ -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(); diff --git a/ui/panel.html b/ui/panel.html index 1ddb734..8b99843 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -516,6 +516,9 @@
+ @@ -610,6 +613,9 @@
+ diff --git a/ui/panel.js b/ui/panel.js index 9b3969b..10f2e57 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -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); @@ -1550,6 +1596,36 @@ function _getMemoryNodeTypeClass(type) { } } +function _parseFloorFilter(raw) { + const text = String(raw || "").trim(); + if (!text) return null; + const ranges = []; + for (const part of text.split(/[,,\s]+/)) { + const rangeParts = part.split(/[-~]/); + if (rangeParts.length === 2) { + const lo = parseInt(rangeParts[0], 10); + const hi = parseInt(rangeParts[1], 10); + if (!Number.isNaN(lo) && !Number.isNaN(hi)) { + ranges.push([Math.min(lo, hi), Math.max(lo, hi)]); + } + } else { + const n = parseInt(part, 10); + if (!Number.isNaN(n)) ranges.push([n, n]); + } + } + return ranges.length ? ranges : null; +} + +function _matchesFloorFilter(node, ranges) { + const seq = node.seq ?? -1; + const seqLo = node.seqRange?.[0] ?? seq; + const seqHi = node.seqRange?.[1] ?? seq; + for (const [lo, hi] of ranges) { + if (seqHi >= lo && seqLo <= hi) return true; + } + return false; +} + function _refreshTaskMemoryBrowser() { const el = document.getElementById("bme-task-memory"); if (!el) return; @@ -1565,6 +1641,7 @@ function _refreshTaskMemoryBrowser() { .trim() .toLowerCase(); const currentFilter = document.getElementById("bme-task-memory-filter")?.value || "all"; + const currentFloorQuery = String(document.getElementById("bme-task-memory-floor")?.value || "").trim(); let nodes = Array.isArray(graph.nodes) ? graph.nodes.filter((node) => !node?.archived) @@ -1587,6 +1664,13 @@ function _refreshTaskMemoryBrowser() { }); } + if (currentFloorQuery) { + const floorFilter = _parseFloorFilter(currentFloorQuery); + if (floorFilter) { + nodes = nodes.filter((node) => _matchesFloorFilter(node, floorFilter)); + } + } + const sorted = nodes.slice().sort((a, b) => { const importanceDiff = (b.importance || 5) - (a.importance || 5); if (importanceDiff !== 0) return importanceDiff; @@ -1624,6 +1708,7 @@ function _refreshTaskMemoryBrowser() {
+