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() {
+