唯杰地图 logo唯杰地图
VJMAP
EN

undo-redo

vjmapext/references/drawing/undo-redo.md 6.0 KB168 行
原文

本文回答: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 → undo
  • Ctrl+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();
}

十、常见坑

  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 也是删旧加克隆。

相关