唯杰地图 logo唯杰地图
VJMAP
EN

group

vjcad/references/undo/group.md 4.6 KB138 行
原文

本文回答:撤销组 = 把多次 *_undoMark 合并成"一次撤销"。两套 API:Engine.startUndoRecord(desc?) / endUndoRecord()(嵌套安全,命令首选)和 undoMgr.start_undoMark(desc?) / end_undoMark()(底层,不嵌套安全)。两者都要 try/finally 保证配对调用。Engine.isInUndoRecord() 检测当前是否在组内。

一、最小用法

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 嵌套示例

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(底层版)

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 标记,撤销时一次只能回滚最内层的组。

五、检查嵌套状态

if (Engine.isInUndoRecord()) {
  console.log('当前在撤销组内');
}

MCP 参考:vjcad/doc/undo/undo-redo.md。

六、命令中的标准模板

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。

七、批量优化(提速 + 单次撤销)

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