乐于分享
好东西不私藏

C++ 源码剖析:LeoCAD是如何实现撤销重做功能的?

C++ 源码剖析:LeoCAD是如何实现撤销重做功能的?

在开发 CAD 或图形编辑软件时,撤销/重做(Undo/Redo) 永远是开发者绕不开的“大山”。 很多开发者第一时间会想到命令模式(Command Pattern)——记录用户的每一个动作(比如:将零件 A 移动了 5 厘米)。但你是否思考过,在复杂的 3D 场景中,如果涉及零件嵌套、群组关联、柔性连接,这种“增量式”记录极易产生逻辑 Bug。

今天,我们深度拆解开源乐高 CAD 软件 LeoCAD 的源码,看看它如何用一种“暴力且优雅”的方式,教科书般地解决了这个问题。


一、 核心逻辑:告别增量,拥抱镜像

通过对 LeoCAD 核心文件 lc_model.cpp 和 lc_model.h 的逐行扫描,我们发现了一个有趣的结构体定义:

structlcModelHistoryEntry
{

    QByteArray File;      // 关键:全量数据副本
    QString Description;  // 操作描述(如 "Undo Move")
};

出乎意料! LeoCAD 并没有采用复杂的增量记录,而是采用了全量内存镜像(Full Memory Snapshot)

当你执行每一个操作(移动、着色、删除)时,LeoCAD 实际上是在内存里为整个模型“拍了一张全家福”。


二、 深度解析:三层栈管理架构

LeoCAD 的撤销系统由一个清晰的“金字塔”结构支撑:

1. 顶层:双栈控制器

在 lcModel 类中,维护着两个 std::vector 容器:mUndoHistory 和 mRedoHistory。 它们存储的是 unique_ptr<lcModelHistoryEntry>,确保了在撤销步骤被移除时,内存能被自动、安全地回收。

2. 中层:快照生成器(SaveCheckpoint)

每当用户完成一个动作,lcModel::SaveCheckpoint 就会被触发。它的工作极其简单粗暴:

  • 序列化:调用 SaveLDraw 将当前场景所有零件、相机、灯光转化为 LDraw 文本流。
  • 持久化:将这串文本流存入 QByteArray 字节数组。
  • 压栈:放入 Undo 栈。

3. 底层:状态恢复器(LoadCheckPoint)

当你按下 Ctrl+Z,魔术发生了:

  • 系统直接清空当前内存中的整个模型。
  • 将 QByteArray 中的数据重新“喂”给解析器,像打开一个新文件一样重建整个场景。

三、 为什么“暴力全量”反而是更好的设计?

对于 CAD 软件,这种“全量快照”逻辑有三大不可替代的优势:

1. 逻辑的绝对鲁棒性

在 3D 编辑中,删除一个父群组可能会导致级联的状态变化。如果使用命令模式,你需要编写极其复杂的逆向逻辑(UndoDeleteGroup)。而使用全量快照,你永远不需要担心“撤销不回去”,因为你回到的就是那个真实的过去。

2. 实现成本极低

LeoCAD 直接复用了现成的文件保存/加载代码(LDraw 协议)。这意味着:只要软件能保存文件,它就自动拥有了撤销功能。

3. 内存与性能的巧妙平衡

虽然是“全量”,但 LDraw 格式基于文本,极为精简。一个包含上千个零件的模型,其文本描述可能也就几十 KB。配合现代计算机 GB 级的内存,保存几百步撤销也毫无压力。


四、 细节见真章:状态检测与脏数据

在源码中,我们还发现了两个体现开发者功底的细节:

  • 变化检测:在保存快照前,LeoCAD 会对比前后状态。如果用户只是点了一下零件但没产生实际位移,快照会被丢弃,避免浪费内存。
  • 脏数据标记mSavedHistory 指针。它指向最后一次点击“保存”时的历史节点。通过对比当前栈顶指针和它的位置,软件能精准地判断标题栏是否需要显示那个代表已修改的星号(*)。

五、 给开发者的启示

LeoCAD 的设计哲学告诉我们:不要过度设计。

在数据规模可控、结构逻辑复杂的场景下,与其为了节省那点内存去编写如履薄冰的增量撤销逻辑,不如利用好已有的序列化机制,做一个稳健、一致的全量快照系统。

毕竟,对于用户来说,一个永不崩溃、撤销必成功的编辑器,才是最好的编辑器。


项目地址:https://github.com/leozide/leocad解析文件lc_model.cpp / lc_model.h


本分析基于 LeoCAD 最新版本源码。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » C++ 源码剖析:LeoCAD是如何实现撤销重做功能的?

猜你喜欢

  • 暂无文章