> **本文回答**：vjcad 撤销系统由 `Engine.undoManager`（`UndoManager`）管理；推荐用 `Engine.startUndoRecord('描述') / endUndoRecord()` 或 `undoMgr.start_undoMark / end_undoMark` 包裹一组操作；底层用 `*_undoMark` 系列方法记录单步（`added_undoMark / erased_undoMark / moved_undoMark / rotate_undoMark / scaled_undoMark / mirrored_undoMark / modEntity_undoMark` 等）。`Engine.addEntities / eraseEntities` 等封装方法**自动记录撤销**。

## 一、按子主题挑

| 想做的 | 子主题 |
|---|---|
| 基本 undo / redo / 状态查询 | [basic](basic.md) |
| 撤销标记（按操作类型分别记录） | [mark](mark.md) |
| 撤销组（`startUndoRecord` 等批量包裹） | [group](group.md) |
| 操作描述（`UndoDescriptor`、历史查询、UI 显示） | [description](description.md) |

## 二、心智模型

```
┌────────────────────────────────────────────────────┐
│ Engine.undoManager (UndoManager)                  │
│   .canUndo() / .canRedo()                          │
│   .undo() / .redo()                                │
│   .start_undoMark(description?)                    │
│   .end_undoMark()                                   │
│   .added_undoMark(entities)                         │
│   .erased_undoMark(entities)                        │
│   .moved_undoMark(entities, from, to)               │
│   .rotate_undoMark(entities, base, angle)           │
│   .scaled_undoMark(entities, base, scale)           │
│   .mirrored_undoMark(entities, p1, p2)              │
│   .modEntity_undoMark(entity)                       │
│   .getUndoHistory() / .getRedoHistory()             │
│   .undoSteps(n|name) / .redoSteps(n|name)           │
│   .rollbackLastGroup()                              │
└────────────────────────────────────────────────────┘
   ↑              ↑
   └──── 推荐：Engine.startUndoRecord('描述') / endUndoRecord()
                  （嵌套安全，等价 start/end_undoMark + 计数）
```

MCP 参考：`vjcad/doc/undo/index.md`、`vjcad/doc/undo/undo-redo.md`。

## 三、最小用法

```js
const undoMgr = Engine.undoManager;

undoMgr.undo();         // 撤销（只改数据，不刷新画布，之后 Engine.regen()）
undoMgr.redo();         // 重做（同上）
undoMgr.canUndo();      // 是否可撤销
undoMgr.canRedo();      // 是否可重做

// 推荐：用撤销组包住一组操作
Engine.startUndoRecord('批量创建');
try {
  for (let i = 0; i < 10; i++) {
    Engine.addEntities(new CircleEnt([i*20, 0], 5));
  }
} finally {
  Engine.endUndoRecord();
}
// 一次 undo 全部回滚
```

MCP 参考：`vjcad/doc/undo/index.md`、`vjcad/doc/undo/undo-redo.md`。

## 四、`Engine.addEntities` / `eraseEntities` 自动撤销

| 封装方法 | 是否自动撤销 |
|---|---|
| `Engine.addEntities(...)` | ✅ |
| `Engine.eraseEntities(...)` | ✅ |
| `Engine.setEntityColor(...)` / `setEntityLayer(...)` | ✅ |
| `Engine.markModified(ent)` 后改属性 | 视使用方式（手动记录）|
| 直接 `Engine.pcanvas.addEntity(...)` | ❌ 需 `added_undoMark([...])` |

MCP 参考：`vjcad/doc/undo/undo-redo.md`。

## 五、`startUndoRecord` vs `start_undoMark`

| | `Engine.startUndoRecord` | `undoMgr.start_undoMark` |
|---|---|---|
| 嵌套 | **安全**（引用计数，仅最外层生效） | **不安全**（每次都 push START） |
| 适用 | 命令中（命令可能互相调用） | 确定不嵌套的 UI 代码 |
| 推荐 | **不确定时优先** | 底层控制 / 性能敏感 |

详见 [group](group.md)。

> **差异**：vjcad 的 undo/redo 恢复**同一对象引用**——`eraseEntities` 只是软删（`isAlive=false`，实体留在 `currentSpace.items`），undo 后置回 `true`，持有旧引用的代码继续有效。这与 vjmapext 的"克隆重建"语义相反；注意 `getEntities()` 不过滤死实体（见 [selection/selection-api](../selection/selection-api.md)）。

MCP 参考：`vjcad/doc/undo/undo-redo.md`。

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/undo/index.md`、`vjcad/doc/undo/undo-redo.md`
- 示例：`vjcad/example/undo/01basic.js` ~ `04description.js`
