Appearance
viewer.history — HistoryManager
设计意图
viewer.history 是统一编辑撤销栈。引擎里能被编辑的东西有四类——零件变换、外观(颜色 / 金属度粗糙度 / 材质预设)、可见性、序号标签,再加上结构删除 / 合并——它们共用一条栈,按时间顺序交错撤销。用户不需要知道上一步动的是哪个域,⌘Z 就是回到上一步。
这是刻意的:早期变换有自己的 undo 栈、可见性另有一套,结果「隐藏一个零件再挪一下,撤销两次却只退回一步」。现在只有一条栈,各域的 undo() / redo()(如 transform.undo())也都转发到它——新代码直接用 viewer.history。
栈里的步都是视图级的,各带一个 viewId 标签。切视图时宿主经 setActiveView(id) 下推当前视图,之后压入的步就归属这个视图。撤销时的「视图对齐」由宿主完成:先 peekUndoScope() 看栈顶那步属于哪个视图,必要时切过去,再 undo()——门面本身不切视图(守层,引擎不该反过来驱动 UI 导航)。
何时用它:接 ⌘Z / ⌘⇧Z 快捷键、驱动撤销 / 重做按钮的启停、永久删除某个视图时清理它的历史步。
典型用法
接快捷键与按钮启停:
ts
viewer.on("history-changed", (e) => {
undoBtn.disabled = !e.canUndo;
redoBtn.disabled = !e.canRedo;
});
undoBtn.onclick = () => viewer.history.undo();
redoBtn.onclick = () => viewer.history.redo();多视图宿主的撤销:先对齐视图,再撤销(否则会撤销到一个用户看不见的视图上):
ts
function undoWithViewAlign() {
const scope = viewer.history.peekUndoScope(); // { viewId } | null(空栈)
if (!scope) return;
if (scope.viewId && scope.viewId !== currentViewId) switchToView(scope.viewId);
viewer.history.undo();
}视图切换与删除时维护标签:
ts
viewer.history.setActiveView(viewId); // 之后压入的步归属该视图
viewer.history.purgeView(deletedViewId); // 永久删除视图 → 剔除其历史步注意事项
- 一条栈,四类步交错:变换 / 外观 / 可见性 / 标签(以及结构删除 / 合并)按操作时间入同一条栈,撤销顺序就是操作的逆序。
- 切视图和应用视图不入栈:
viewer.views的apply/activate走的是不记录 undo 的批量还原原语——导航不是编辑。 canUndo()/canRedo()是查询,history-changed是通知:UI 用事件驱动启停,不要每帧轮询。peekUndoScope()空栈返回null;返回的{ viewId }里viewId可能为null(该步压入时没有活动视图)。setActiveView只打标签,不切视图:真正的视图切换在viewer.views;两者需要宿主保持同步。- 各域的
undo()是同一条栈的别名:viewer.transform.undo()与viewer.history.undo()等价,不要以为它们是两条独立历史。 - 换模型清空历史:
model-disposed后栈重置,canUndo()回 false。
完整签名与延伸
HistoryManager— 全部方法的精确签名。EditHistorySnapshot/SerializedEditStep— 历史的可序列化形状(宿主要把撤销栈随文档存盘时用)。- 指南:编辑能力 — 三类编辑与统一撤销栈的全景。
- 相邻域:viewer.transform、viewer.labels、viewer.model(可见性 / 外观 / 结构编辑)。