Appearance
状态: 🟢 已修复
日期: 2026-07-15 严重程度: 🔴 P1 影响范围:
frontend/src/vmd-loader.ts,frontend/src/proc-motion-bridge.ts,frontend/src/thumbnail-capture.ts发现方式: 用户反馈 修复方案: stopProcMotion() 从 vmdData 赋值前移到赋值后,确保缩略图截帧时 userVmdPresent=true
程序化动作切换与缩略图截帧时序冲突
日期: 2026-07-15 严重程度: P1(缩略图截到空姿态,用户可见) 影响范围: vmd-loader.ts(VMD 加载)+ proc-motion-bridge.ts(程序化动作停止)+ thumbnail-capture.ts(缩略图渲染) 发现方式: 用户观察 + 邻组 6 轮修复未果
问题描述
加载真实 VMD 动作时,缩略图截到空姿态(角色静止 T-Pose 或空白帧),而非动作初始帧。
触发条件:程序化动作(idle/autodance)激活状态下加载真实 VMD。
根因分析
三层时序叠加导致的隐蔽 bug:
stopProcMotion()内部有userVmdPresent守卫(inst.vmdData !== null),用于防止已有 VMD 时误清动画- 但
stopProcMotion()在inst.vmdData赋值之前调用(vmd-loader.ts:82) - 缩略图截帧发生在
stopProcMotion()之后、vmdData赋值之前
时序图:
stopProcMotion() → userVmdPresent=false → 清空动画
缩略图截帧 → 动画已空 → 截到空姿态
inst.vmdData = data → 此时赋值已晚为什么隐蔽
- 跨文件状态依赖:
stopProcMotion()在proc-motion-bridge.ts,vmdData赋值在vmd-loader.ts,状态读取在config.ts——三处各管一段 - 触发条件窄:只在"程序化动作激活 + 加载真实 VMD"时复现,普通测试不会同时开 autoSwitch 又截缩略图
userVmdPresent守卫是后加的修复:原本stopProcMotion()无条件清动画,守卫是为了防止"已有 VMD 时被程序化动作覆盖",但引入了新的时序依赖- 代码读起来合理:字面上"先停程序化、再绑 VMD、再截图"是正确顺序,但
userVmdPresent的检查把副作用延迟到了绑 VMD 之后
修复方案
将 stopProcMotion() 从 inst.vmdData 赋值前移到赋值后:
typescript
// vmd-loader.ts
inst.vmdData = data;
// 停止程序化动作(延迟到 vmdData 赋值后,确保 userVmdPresent=true)
if (isProcVmdActive() && name !== PROC_VMD_NAME_IDLE && name !== PROC_VMD_NAME_AUTODANCE) {
stopProcMotion();
}这样缩略图截帧时 userVmdPresent=true,动画不会被清空。
教训
- 异步时序假象:代码读起来像"先 A 再 B 再 C",但 A 的副作用依赖 C 之后才赋值的状态
- 守卫条件的隐式依赖:
userVmdPresent守卫假设vmdData已赋值,但调用点在赋值之前 - 跨文件状态读取的盲区:改一处看似合理,但另一处的隐式依赖就断了