Skip to content

Commit 62a0052

Browse files
wuyehanHongzhi Wenclaude
authored
修复角色卡面板二次打开时 Live2D 预览卡死问题 (Project-N-E-K-O#999)
* fix: restore character card manager preview behavior * fix: address workshop sync review comments * fix: normalize workshop card faces and stabilize live2d preview * fix: address PR 986 review feedback * fix: preserve legacy card faces without sidecar * fix: suppress stale live2d preview load errors * fix: cancel stale live2d loads when switching 3d previews * fix: restore live2d preview playback after steam load * test: cover workshop card face refresh guards * fix: address PR 988 review feedback * fix: harden workshop card face sync writes * fix: count workshop face sync backfill errors * fix: reset live2d preview context on panel reopen * fix: address PR 999 coderabbit feedback - Cancel pending Steam tab init timer on panel close so the 500ms delayed buildSteamTabContent cannot rebuild the Live2D preview after destroyLive2DPreviewContext has run. - Replace the misleading removeModel(force, options) signature with an options-only API; force=true previously *skipped* window cleanup, the inverse of what the name implied. All 5 callers now pass { skipCloseWindows: true } explicitly. - Tighten the panel-close regression test to assert that the first pixi_app.destroy() actually fired, locking in the destroy contract in destroyLive2DPreviewContext. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address PR 999 coderabbit follow-up - Pull ticker.stop() out of the bundled UI-cleanup try in removeModel so that an exception from removeChild / ticker.remove cannot leave the ticker running while activeModel.destroy() executes. - Move _mouthTicker = null out of its `if` block to match the unconditional null assignment used for _lockIconTicker / _floatingButtonsTicker, so all three ticker fields drop their references uniformly. - Loosen canvasDisplayAfterReopen assertion to "!= 'none'", matching the existing test in the same file and tolerating an explicit "block" value if loadLive2DModelByName ever sets one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d52dfba commit 62a0052

3 files changed

Lines changed: 411 additions & 145 deletions

File tree

static/js/character_card_manager.js

Lines changed: 98 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4141,14 +4141,18 @@ function openCatgirlPanel(card, originEl) {
41414141
setTimeout(_resizeAllPanelTextareas, 500);
41424142

41434143
// 延迟初始化 Steam 标签页内容(等待面板展开动画完成后)
4144+
// 用 overlay 持有 timer id,关闭时统一 clearTimeout,避免在 closing 期间重建预览
41444145
if (!isNew) {
4145-
setTimeout(() => {
4146+
const steamInitTimer = setTimeout(() => {
4147+
overlay._steamTabInitTimer = null;
4148+
if (overlay.dataset.closing === 'true' || !overlay.isConnected) return;
41464149
const steamContainer = rightSection.querySelector('.panel-tab-steam');
41474150
if (steamContainer && !steamContainer.dataset.initialized) {
41484151
steamContainer.dataset.initialized = 'true';
41494152
buildSteamTabContent(name, rawData, card, steamContainer);
41504153
}
41514154
}, 500);
4155+
overlay._steamTabInitTimer = steamInitTimer;
41524156
}
41534157
}, 500);
41544158
});
@@ -4160,15 +4164,25 @@ function openNewCatgirlPanel() {
41604164
}
41614165
window.openNewCatgirlPanel = openNewCatgirlPanel;
41624166

4163-
function closeCatgirlPanel() {
4167+
async function closeCatgirlPanel() {
41644168
const overlay = document.querySelector('.catgirl-panel-overlay');
41654169
if (!overlay) return;
4170+
if (overlay.dataset.closing === 'true') return;
4171+
overlay.dataset.closing = 'true';
4172+
4173+
// 取消尚未触发的 Steam 标签页延迟初始化,避免在清理后又把预览建回来
4174+
if (overlay._steamTabInitTimer) {
4175+
clearTimeout(overlay._steamTabInitTimer);
4176+
overlay._steamTabInitTimer = null;
4177+
}
41664178

41674179
// 清理模型预览资源(如果 Steam 标签页曾加载过)
41684180
try {
4169-
if (typeof disposeWorkshopVrm === 'function') disposeWorkshopVrm();
4170-
if (typeof disposeWorkshopMmd === 'function') disposeWorkshopMmd();
4171-
if (typeof clearLive2DPreview === 'function') clearLive2DPreview();
4181+
const cleanupTasks = [];
4182+
if (typeof disposeWorkshopVrm === 'function') cleanupTasks.push(disposeWorkshopVrm());
4183+
if (typeof disposeWorkshopMmd === 'function') cleanupTasks.push(disposeWorkshopMmd());
4184+
if (typeof destroyLive2DPreviewContext === 'function') cleanupTasks.push(destroyLive2DPreviewContext());
4185+
await Promise.allSettled(cleanupTasks);
41724186
} catch (e) {
41734187
console.warn('[Panel] 清理预览资源时出错:', e);
41744188
}
@@ -4179,14 +4193,12 @@ function closeCatgirlPanel() {
41794193
wrapper.classList.add('phase-center');
41804194
}
41814195

4182-
setTimeout(() => {
4183-
overlay.classList.remove('active');
4184-
if (wrapper) wrapper.classList.remove('phase-center');
4185-
setTimeout(() => {
4186-
overlay.remove();
4187-
_catgirlPanelOpen = false;
4188-
}, 400);
4189-
}, 300);
4196+
await new Promise(resolve => setTimeout(resolve, 300));
4197+
overlay.classList.remove('active');
4198+
if (wrapper) wrapper.classList.remove('phase-center');
4199+
await new Promise(resolve => setTimeout(resolve, 400));
4200+
overlay.remove();
4201+
_catgirlPanelOpen = false;
41904202
}
41914203
window.closeCatgirlPanel = closeCatgirlPanel;
41924204

@@ -6914,7 +6926,7 @@ async function loadVrmPreview(modelPath, rawData) {
69146926

69156927
// 清理 Live2D 预览(如果有)
69166928
if (live2dPreviewManager && live2dPreviewManager.currentModel) {
6917-
await live2dPreviewManager.removeModel(true);
6929+
await live2dPreviewManager.removeModel({ skipCloseWindows: true });
69186930
currentPreviewModel = null;
69196931
}
69206932

@@ -7018,7 +7030,7 @@ async function loadMmdPreview(modelPath, rawData) {
70187030

70197031
// 清理 Live2D 预览(如果有)
70207032
if (live2dPreviewManager && live2dPreviewManager.currentModel) {
7021-
await live2dPreviewManager.removeModel(true);
7033+
await live2dPreviewManager.removeModel({ skipCloseWindows: true });
70227034
currentPreviewModel = null;
70237035
}
70247036

@@ -7139,11 +7151,12 @@ async function clearLive2DPreview(showModelNotSetMessage = false) {
71397151
try {
71407152
cancelPendingLive2DPreviewLoads();
71417153
selectedModelInfo = null;
7154+
window._previewMotionFiles = [];
71427155
setLive2DPreviewRefreshButtonState(false, false);
71437156

71447157
// 如果有模型加载,先移除它
7145-
if (live2dPreviewManager && live2dPreviewManager.currentModel) {
7146-
await live2dPreviewManager.removeModel(true);
7158+
if (live2dPreviewManager && typeof live2dPreviewManager.removeModel === 'function') {
7159+
await live2dPreviewManager.removeModel({ skipCloseWindows: true });
71477160
}
71487161
currentPreviewModel = null;
71497162

@@ -7187,6 +7200,72 @@ async function clearLive2DPreview(showModelNotSetMessage = false) {
71877200
}
71887201
}
71897202

7203+
async function destroyLive2DPreviewContext() {
7204+
const manager = live2dPreviewManager;
7205+
cancelPendingLive2DPreviewLoads();
7206+
selectedModelInfo = null;
7207+
currentPreviewModel = null;
7208+
window._previewMotionFiles = [];
7209+
setLive2DPreviewRefreshButtonState(false, false);
7210+
7211+
if (!manager) {
7212+
return;
7213+
}
7214+
7215+
if (typeof manager._activeLoadToken === 'number') {
7216+
manager._activeLoadToken += 1;
7217+
}
7218+
7219+
try {
7220+
await clearLive2DPreview();
7221+
} finally {
7222+
manager._isLoadingModel = false;
7223+
manager._modelLoadState = 'idle';
7224+
manager._isModelReadyForInteraction = false;
7225+
7226+
if (manager._canvasRevealTimer) {
7227+
clearTimeout(manager._canvasRevealTimer);
7228+
manager._canvasRevealTimer = null;
7229+
}
7230+
7231+
try {
7232+
if (manager.pixi_app && manager.pixi_app.view && manager.pixi_app.view.style) {
7233+
manager.pixi_app.view.style.transition = '';
7234+
manager.pixi_app.view.style.opacity = '';
7235+
}
7236+
} catch (_) {}
7237+
7238+
if (manager._previewResizeHandlerBound && manager._previewResizeHandler) {
7239+
window.removeEventListener('resize', manager._previewResizeHandler);
7240+
}
7241+
manager._previewResizeHandlerBound = false;
7242+
manager._previewResizeHandler = null;
7243+
7244+
if (manager._screenChangeHandler) {
7245+
window.removeEventListener('resize', manager._screenChangeHandler);
7246+
manager._screenChangeHandler = null;
7247+
}
7248+
if (manager._displayChangeHandler) {
7249+
window.removeEventListener('electron-display-changed', manager._displayChangeHandler);
7250+
manager._displayChangeHandler = null;
7251+
}
7252+
7253+
if (manager.pixi_app && typeof manager.pixi_app.destroy === 'function') {
7254+
try {
7255+
manager.pixi_app.destroy(true);
7256+
} catch (destroyError) {
7257+
console.warn('[CharacterCard] 销毁 Live2D 预览 PIXI 实例失败:', destroyError);
7258+
}
7259+
}
7260+
7261+
manager.pixi_app = null;
7262+
manager.currentModel = null;
7263+
manager.isInitialized = false;
7264+
manager._lastPIXIContext = { canvasId: null, containerId: null };
7265+
live2dPreviewManager = null;
7266+
}
7267+
}
7268+
71907269
// 通过模型名称加载Live2D模型
71917270
async function loadLive2DModelByName(modelName, modelInfo = null) {
71927271
const loadGeneration = beginLive2DPreviewLoadGeneration();
@@ -7199,7 +7278,7 @@ async function loadLive2DModelByName(modelName, modelInfo = null) {
71997278

72007279
if (loadedModel && live2dPreviewManager?.currentModel === loadedModel) {
72017280
try {
7202-
await live2dPreviewManager.removeModel(true);
7281+
await live2dPreviewManager.removeModel({ skipCloseWindows: true });
72037282
} catch (cleanupError) {
72047283
console.warn('[CharacterCard] 清理过期 Live2D 预览失败:', cleanupError);
72057284
}
@@ -7231,7 +7310,7 @@ async function loadLive2DModelByName(modelName, modelInfo = null) {
72317310

72327311
// 如果已经有模型加载,先移除它
72337312
if (live2dPreviewManager && live2dPreviewManager.currentModel) {
7234-
await live2dPreviewManager.removeModel(true);
7313+
await live2dPreviewManager.removeModel({ skipCloseWindows: true });
72357314
// 重置当前预览模型引用
72367315
currentPreviewModel = null;
72377316
}

0 commit comments

Comments
 (0)