Skip to content

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.viewsapply / 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。

完整签名与延伸