group
本文回答:撤销组 = 把多次
*_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