undo-redo
本文回答:
UndoManager.beginTransaction / endTransaction / undo / redo—— 把一组操作打包成一次撤销单元。
一、基础 API
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 快照再改:
um.recordModify(ent); // 改之前!克隆当前状态入栈
ent.color = 0xff0000;
ent.changed();
事务把一批操作打包成一条 undo:
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 → 全部还原
三、和命令的配合
自定义命令一般这样包:
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 一次撤销整个"画多段"。
四、嵌套事务
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 快照——纯渲染态修改本来就不入栈:
ent.effectOpacity = 0.5; // effect* 系列只影响显示,不产生 undo 记录
ent.styleChanged();
所以"选择高亮切换"这类操作直接改 effectXxx 即可,无需特殊处理。
六、Undo 状态同步
没有 undo:changed 事件;真实发射的是 undo:performed / redo:performed(UndoManager 直发、无 payload、不在 VjmapextEvents 枚举内,用字面量订阅)。UI 刷新(如撤销/重做按钮启用态):
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→ undoCtrl+Y→ redo(没有Ctrl+Shift+Z绑定,ShortcutMap 只有一个redo槽位)
// 查询(返回方向:动作 → 键位)
mapcadLayer.getShortcuts();
// { undo: 'ctrl+z', redo: 'ctrl+y', selectAll: 'ctrl+a', modeToggle: 'ctrl+shift+e', ... }
九、典型场景
1 插入一块(整个作为 1 条 undo)
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 批量编辑后一次撤销
um.beginTransaction(); // 批量缩放;scale(center, factor)
selection.forEach(e => { um.recordModify(e); e.scale(basePoint, 2); }); // 先快照再改
um.endTransaction();
3 命令失败回滚
命令逻辑里如果中途抛错,endTransaction 仍要执行;建议用 try/finally:
um.beginTransaction(); // 命令
try {
...
} finally {
um.endTransaction();
}
十、常见坑
- begin 没 end → 后续所有操作都往这条 undo 里挤,一次 undo 撤掉大片。
- endTransaction 前抛异常:未释放;用 try/finally。
- 循环里 begin / end 每次都开:一次 undo 只撤最后一条;应在外层 begin/end。
- 程序化修改忘
recordModify:changed()只标脏不入 undo 栈;要可撤销须在改之前um.recordModify(ent)(内建交互自动做,自定义代码不做)。 - Undo 后 Marker/popup 残留:Marker 不是实体,不在 undo 里;手动 remove。
- 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
- 批量与事务 → batch/entity-bulk-add
- 序列化保存 → serialize