Appearance
场景的底片
背景:场景保存与恢复断裂——自动保存防抖、上次场景恢复、撤销快照各自为政,删除模型后快照不可回退。 过程:SceneFile 序列化 + last-scene restore + pushUndoSnapshot/popUndoSnapshot 链路打通,删除操作前置快照。
桌面壳摆好了一个场景。
三个模型,一束灯光,一片天空,一段正在播放的动作。它满意地看着屏幕——这个画面值得被记住。
"保存。"
然后它做了一件残酷的事:删掉了一个模型。
"撤销。"
画面纹丝不动。被删掉的模型没有回来。
"撤销!" 桌面壳又按了一次。
什么也没有发生。
它打开控制台,看到一行字:No undo snapshot available。没有快照,就没有后悔药。它的"保存"和它的"撤销"是两个互不相识的系统——保存把场景写进了硬盘,撤销却不知道这一幕曾经存在过。
"我能保存整个场景," 桌面壳说,"却不能在删掉一个模型后,把它找回来。保存是写给未来的,撤销是写给过去的——为什么它们不能是同一个东西?"
一、底片
桌面壳找到了序列化器。
SceneFile——一个把整个场景打包成数据结构的信封:模型列表、材质、环境参数、场景级动作、播放状态。它像一张底片:把三维世界的一切,压成一行一行的数据。
typescript
export function serializeScene(): SceneFile {
// 遍历模型注册表 → 收集 modelId / 路径 / 材质覆盖 / 动作配置
// 收集 envState(天空、地面、雾、云、水、粒子)
// 收集场景级动作(motion-intent 的场景动作库)
// 返回 SceneFile —— 一张完整的底片
}桌面壳把这张底片摊开看。它惊讶地发现,场景里的一切都被收录了——不只是模型的位置和旋转,还有材质的覆盖、粒子系统的开关、甚至正在播放的动作和它的进度。
"这是一张会呼吸的底片。" 桌面壳说,"它记住的不只是'有什么',还有'正在发生什么'。"
它对照着把底片写回世界:
typescript
export async function deserializeScene(data: SceneFile, skipEnv = false): Promise<number> {
// 反序还原:先清空旧场景 → 再逐个重建模型 → 恢复环境 → 恢复动作
// 返回还原的模型数量
}"保存是照一张相,恢复是把照片里的世界重新搭出来。" 桌面壳说,"序列化是摄影师,反序列化是搭景工——同一张底片,两个工种。"
二、记忆的写法
但桌面壳很快发现,这张底片有个毛病:它太容易被冲走。
用户改一个滑块、挪一个模型、切一个动作——每次变化,都触发一次自动保存。自动保存是防抖的:debounce 把高频变化攒成一波,停手后才真正落盘。
"防抖是好的," 桌面壳说,"不然拖一个滑块能触发三十次保存,硬盘都要冒烟。但防抖也有代价——它只记'现在',不记'刚才'。"
"什么意思?"
"保存是覆盖式的。每次落盘,都拿最新的底片盖住旧的那张。你删掉一个模型,然后保存——旧底片上还有它,新底片已经没有了。防抖让你少写几次硬盘,也让'过去'被冲得干干净净。"
"那怎么办?"
"在覆盖之前,先留一张底片。" 桌面壳说,"不是每次保存都留——那会撑爆硬盘。只在'破坏性操作'之前留:删模型、卸舞台、清预设。留一张快照,让用户反悔时有路可退。"
三、后悔药的配方
桌面壳实现了三样东西:
typescript
pushUndoSnapshot(): string | null; // 破坏性操作前:按下快门,留一张底片
popUndoSnapshot(): string | null; // 撤销时:取出最近一张底片
restoreUndoSnapshot(snap: string): Promise<boolean>; // 把底片写回世界"这三样东西,分别对应胶片相机的三个动作:按下快门、掀开暗盒、把照片放回世界。"
它在删除模型的入口加了一行:
typescript
const snap = pushUndoSnapshot(); // 删除前,先留一张底片
removeSceneMotion(motion.id); // 然后才动手
offerSceneUndo(t('motion.motionRemoved'), snap, () => menu.reRender());"offerSceneUndo 把后悔药递给用户:一个 toast,一个按钮。用户点'撤销',restoreUndoSnapshot 把底片上的世界重新搭起来——被删的模型,从数据里回来了。"
"快照会过期吗?"
"会。快照是字符串,存的是序列化数据。它占内存——所以栈有深度限制,只能反悔有限步。你删了十个模型,最老的那张底片早就被挤出去了。"
"那是最新的能反悔,最旧的反悔不了?"
"对。后悔药的保质期是有限的——越早的遗憾,越难挽回。这很公平,也提醒人:不要等到删了十个才想起来后悔。"
四、重启之后
还有一件事,桌面壳一直没想明白。
它重启了应用。空荡荡的场景——之前摆好的三个模型、灯光、天空,全都不见了。
"我保存过!" 它说,"为什么重启就没了?"
"你保存了,但没让它在启动时找回来。" 外交官说,"保存是写,恢复是读。你只做了写的一半。"
桌面壳找到了 tryRestoreLastScene——启动时自动恢复上次场景的入口。
typescript
export async function tryRestoreLastScene(): Promise<void> {
// 读 last-scene 缓存 → deserializeScene → 恢复上次的世界
}"启动时,先看看硬盘里有没有上一张底片。有,就把它冲印出来——世界回到上次的样子。没有,就空白开场。"
"这算不算'场景的记忆'?"
"算。" 桌面壳说,"模型是活的,动作是活的,但场景本身是脆弱的——断电、崩溃、误删,任何一个都能让它蒸发。底片就是场景的免疫系统:每次保存都是一次疫苗,每次恢复都是一次复活。"
尾声
桌面壳重新摆好了场景。三个模型,一束灯光,一片天空,一段正在播放的动作。
它删掉了一个模型。
"撤销。"
模型回来了——从底片上。位置分毫不差,灯光还是那盏,天空还是那片。整个世界的状态,被一张字符串快照完整地还了回来。
它重启了应用。
世界自己搭了回来——上次的模型、灯光、天空、动作进度,都躺在原地等它。
"保存是写给未来的," 桌面壳说,"撤销是写给过去的,恢复是写给下一次启动的。它们终于是一个东西了——都是把世界压成一行数据,再把它铺回现实。"
"那这行数据是什么?"
"是底片。" 桌面壳说,"场景是光,数据是银盐。光会消失,但银盐记得。"
它看着屏幕上的世界——被保存、被撤销、被恢复,来来回回,总有一张底片接住它。
"世界不是脆弱的," 它轻声说,"只要你还留着它的底片。"
教训:保存是写,撤销是读,恢复是重读——三件事共用同一张底片,才叫完整的记忆。
破坏性操作之前,先留一张后悔药。