> **本文回答**：`UndoManager.beginTransaction / endTransaction / undo / redo` —— 把一组操作打包成一次撤销单元。

## 一、基础 API

```js
const um = mapcadLayer.undoManager;

um.undo();           // 回退一步
um.redo();           // 重做
um.canUndo;          // boolean
um.canRedo;          // boolean
um.clear();          // 清空历史
```

## 二、事务（Transaction）

`addEntity` / `deleteEntity` 会自动入栈一条 undo（`store.add`/`store.delete` 内部调 `recordAdd`/`recordDelete`）。
**修改不自动入栈**——`recordModify` 只由内建交互（grip 拖拽、键盘移动 / 旋转 / 缩放、MOVE/COPY 等命令）在改之前调用。程序化修改要**自己先 `recordModify` 快照再改**：

```js
um.recordModify(ent);     // 改之前！克隆当前状态入栈
ent.color = 0xff0000;
ent.changed();
```

**事务**把一批操作打包成一条 undo：

```js
um.beginTransaction();   // 注意：无参数（无标签入参，d.ts 声明 beginTransaction(): void）

mapcadLayer.getEntities()
  .filter(e => e.layerId === '标注')   // 实体上是 layerId（图层名字符串），不是 layer
  .forEach(e => { um.recordModify(e); e.color = 0xff0000; e.styleChanged(); });

um.endTransaction();

// 用户按一次 Ctrl+Z → 全部还原
```

## 三、和命令的配合

自定义命令一般这样包：

```js
class DrawMultiLineCmd {
  async main(ctx) {   // 实例方法，不能写 static
    const { inputManager: im, store, undoManager: um } = ctx;

    um.beginTransaction();

    while (true) {
      const p = await im.getPoint({ message: '指定点或右键结束' });
      if (p.status !== 'ok') break;
      const line = buildLine(p.value);
      store.add(line);
    }

    um.endTransaction();
  }
}
```

用户按 Ctrl+Z 一次撤销整个"画多段"。

## 四、嵌套事务

```js
um.beginTransaction();  // 外层
  um.beginTransaction();  // 内层
    ...
  um.endTransaction();    // 内层结束，不真入栈
um.endTransaction();      // 外层结束，整个打包入栈
```

只有最外层的 endTransaction 真正把合并后的操作入 undo 栈。

## 五、不希望入 undo 的操作

**没有公开的 `withoutRecording` API**（d.ts 中无此方法；内部 `_silent` 为私有字段）。记录触发点：`store.add`→`recordAdd`、`store.delete`→`recordDelete` 自动触发；`recordModify` **不自动**——`ent.changed()`/`styleChanged()` 都不产生记录，修改要么由内建交互发起、要么自己先 `recordModify` 快照——**纯渲染态修改本来就不入栈**：

```js
ent.effectOpacity = 0.5;   // effect* 系列只影响显示，不产生 undo 记录
ent.styleChanged();
```

所以"选择高亮切换"这类操作直接改 `effectXxx` 即可，无需特殊处理。

## 六、Undo 状态同步

没有 `undo:changed` 事件；真实发射的是 `undo:performed` / `redo:performed`（`UndoManager` 直发、无 payload、不在 `VjmapextEvents` 枚举内，用字面量订阅）。UI 刷新（如撤销/重做按钮启用态）：

```js
function refreshUndoBtns() {
  myUndoBtn.disabled = !mapcadLayer.undoManager.canUndo;   // 你自己的 UI 按钮
  myRedoBtn.disabled = !mapcadLayer.undoManager.canRedo;
}
mapcadLayer.eventBus.on('undo:performed', refreshUndoBtns);
mapcadLayer.eventBus.on('redo:performed', refreshUndoBtns);
mapcadLayer.eventBus.on(VjmapextEvents.CommandEnded, refreshUndoBtns);
```

⚠️ 不要挂 `VjmapextEvents.StoreChanged`——该枚举成员在 1.0.1 从未发射（死枚举）。

## 七、历史栈上限

栈深由内部 `maxStackSize`（私有字段）控制，**没有公开配置项**——`um.maxHistory` 不存在（vjmapext@1.0.1 为 undefined）。超过上限时最早的记录被丢弃。

## 八、键盘快捷键

默认内置：

- `Ctrl+Z` → undo
- `Ctrl+Y` → redo（**没有** `Ctrl+Shift+Z` 绑定，ShortcutMap 只有一个 `redo` 槽位）

```js
// 查询（返回方向：动作 → 键位）
mapcadLayer.getShortcuts();
// { undo: 'ctrl+z', redo: 'ctrl+y', selectAll: 'ctrl+a', modeToggle: 'ctrl+shift+e', ... }
```

## 九、典型场景

### 1 插入一块（整个作为 1 条 undo）

```js
um.beginTransaction();  // 插入块
// 第 1 参是 blockId（blocks.add 时生成的随机 id），不是块名；按名反查
const ref = new vjmapext.BlockRefEnt(mapcadLayer.blocks.itemByName('灯具').blockId, pt);   // (blockId, insertionPoint, scaleFactor?, rotation?)
mapcadLayer.addEntity(ref);
um.endTransaction();
```

### 2 批量编辑后一次撤销

```js
um.beginTransaction();  // 批量缩放；scale(center, factor)
selection.forEach(e => { um.recordModify(e); e.scale(basePoint, 2); });  // 先快照再改
um.endTransaction();
```

### 3 命令失败回滚

命令逻辑里如果中途抛错，endTransaction 仍要执行；建议用 try/finally：

```js
um.beginTransaction();  // 命令
try {
  ...
} finally {
  um.endTransaction();
}
```

## 十、常见坑

1. **begin 没 end** → 后续所有操作都往这条 undo 里挤，一次 undo 撤掉大片。
2. **endTransaction 前抛异常**：未释放；用 try/finally。
3. **循环里 begin / end 每次都开**：一次 undo 只撤最后一条；应在外层 begin/end。
4. **程序化修改忘 `recordModify`**：`changed()` 只标脏不入 undo 栈；要可撤销须在改之前 `um.recordModify(ent)`（内建交互自动做，自定义代码不做）。
5. **Undo 后 Marker/popup 残留**：Marker 不是实体，不在 undo 里；手动 remove。
6. **undo/redo 后实体引用失效**：`undo`/`redo` 内部是 `store.delete(id)` + `store.add(snapshot.clone())`——恢复回来的是**同 id 的克隆新对象**，原来持有的实体引用已脱离 store，对它改属性/删它都不再生效。之后操作一律按 id 重取：`cad.store.get(ent.id)` / `getEntities().find(e=>e.id===id)`。同理 `modify` 的 undo 也是删旧加克隆。

## 相关

- 事务与事件 → [event/bus-command-mode](../event/bus-command-mode.md)
- 批量与事务 → [batch/entity-bulk-add](../batch/entity-bulk-add.md)
- 序列化保存 → [serialize](serialize.md)
