> **本文回答**：撤销组 = 把多次 `*_undoMark` 合并成"一次撤销"。两套 API：`Engine.startUndoRecord(desc?) / endUndoRecord()`（**嵌套安全**，命令首选）和 `undoMgr.start_undoMark(desc?) / end_undoMark()`（底层，不嵌套安全）。两者都要 `try/finally` 保证配对调用。`Engine.isInUndoRecord()` 检测当前是否在组内。

## 一、最小用法

```js
import { Engine, LineEnt, CircleEnt } from 'vjcad';

Engine.startUndoRecord('批量创建');
try {
  Engine.addEntities(new LineEnt([0, 0], [100, 0]));
  Engine.addEntities(new LineEnt([100, 0], [100, 100]));
  Engine.addEntities(new CircleEnt([50, 50], 30));
} finally {
  Engine.endUndoRecord();
}

// 一次 undo 全部回滚（undo 不刷新画布，需自己 regen）
Engine.undoManager.undo();
Engine.regen();
```

MCP 参考：`vjcad/doc/undo/index.md`、`vjcad/doc/undo/undo-redo.md`、`vjcad/example/undo/03group.js`。

## 二、`startUndoRecord` vs `start_undoMark`

| | `Engine.startUndoRecord` | `undoMgr.start_undoMark` |
|---|---|---|
| 嵌套调用 | **安全**（引用计数，仅最外层生效） | **不安全**（每次都 push START 标记） |
| 推荐场景 | 命令中（命令可能互相调用） | 确定不嵌套的 UI（属性面板、对话框） |

MCP 参考：`vjcad/doc/undo/undo-redo.md`、`vjcad/example/undo/04description.js`。

## 三、`Engine.startUndoRecord` 嵌套示例

```js
Engine.startUndoRecord('外层操作');

  Engine.addEntities(entity1);

  Engine.startUndoRecord('内层操作');   // 计数+1，不创建新组
    Engine.addEntities(entity2);
  Engine.endUndoRecord();              // 计数-1，不结束组

  Engine.addEntities(entity3);

Engine.endUndoRecord();                // 计数归 0，真正结束组

// 只有最外层生效，整个 entity1 + entity2 + entity3 是一个撤销单元
```

MCP 参考：`vjcad/example/undo/04description.js`。

## 四、`undoMgr.start_undoMark`（底层版）

```js
const u = Engine.undoManager;

u.start_undoMark('修改属性');
try {
  u.modEntity_undoMark(ent);
  ent.color = 1;
  ent.setModified();
} finally {
  u.end_undoMark();
}
```

> ⚠️ 不嵌套安全。如果 inner 也用 `start_undoMark`，会创建多个 START 标记，撤销时一次只能回滚最内层的组。

## 五、检查嵌套状态

```js
if (Engine.isInUndoRecord()) {
  console.log('当前在撤销组内');
}
```

MCP 参考：`vjcad/doc/undo/undo-redo.md`。

## 六、命令中的标准模板

```js
class MyCommand {
  async main() {
    Engine.startUndoRecord('我的命令');     // 描述会显示在历史 / tooltip
    let ok = false;
    try {
      await this.doWork();
      ok = true;
    } finally {
      Engine.endUndoRecord();              // 先闭合组
      // 失败时回滚整组：必须在组闭合后、且不在外层组内时才回滚
      if (!ok && !Engine.isInUndoRecord()) {
        Engine.undoManager.rollbackLastGroup();
        Engine.regen();
      }
    }
  }
}
```

> ⚠️ 不要在 `catch` 里（组尚未闭合时）调 `undo()`：栈顶不是 END 标记时 `undo()` 只弹出**最后一条**记录，`finally` 再压入 END，留下半个组。

MCP 参考：`vjcad/doc/undo/index.md`。

## 七、批量优化（提速 + 单次撤销）

```js
Engine.startUndoRecord('批量加 1000 个');
Engine.eventManager.suspendEvents();      // 暂停事件（见下方注意）
try {
  for (let i = 0; i < 1000; i++) {
    Engine.addEntities(new CircleEnt([i*10, 0], 5));
  }
} finally {
  Engine.eventManager.resumeEvents();
  Engine.endUndoRecord();
}

Engine.undoManager.undo();                // 一次回滚 1000 个
Engine.regen();                           // undo 不刷新画布
```

> ⚠️ `suspendEvents()` 默认**丢弃**暂停期间的所有事件（包括关联标注 / 约束依赖的 Reactor 桥所需的 `EntityAdded/Modified/Erased`），也**不会合并重绘**；需要恢复时补发用 `suspendEvents(true)`（排队，`resumeEvents()` 时依次派发）。

MCP 参考：`vjcad/doc/event/event-usage.md`、`vjcad/doc/undo/undo-redo.md`。

## 反例

- 没用 `try/finally`：`addEntities` 抛异常 → 撤销组永远没关 → 后续操作全归到这个组。
- 嵌套时混用 `startUndoRecord` 和 `start_undoMark`：行为不可预期。
- `start_undoMark` 后忘 `end_undoMark` → 撤销栈错乱。

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/undo/index.md`、`vjcad/doc/undo/undo-redo.md`
- 示例：`vjcad/example/undo/03group.js`、`04description.js`
