撤销重做
大约 10 分钟
撤销重做
WebCAD提供完整的撤销重做机制,支持各类操作的回退和重做。撤销系统是保证用户操作安全的重要功能。
概述
UndoManager
UndoManager是撤销重做系统的核心类,负责管理撤销和重做操作。
import { Engine, UndoManager } from 'vjcad';
// 获取撤销管理器
const undoMgr: UndoManager = Engine.undoManager;
// 执行撤销
undoMgr.undo();
// 执行重做
undoMgr.redo();
// 检查状态
const canUndo = undoMgr.canUndo();
const canRedo = undoMgr.canRedo();撤销记录类型
添加实体
import { Engine, LineEnt } from 'vjcad';
// 创建并添加实体(推荐:使用addEntities自动记录撤销)
const line = new LineEnt([0, 0], [100, 100]);
line.setDefaults();
Engine.addEntities(line);
// 或者手动记录撤销
const line2 = new LineEnt([0, 0], [50, 50]);
line2.setDefaults();
Engine.pcanvas.addEntity(line2);
Engine.undoManager.added_undoMark([line2]);删除实体
import { Engine, EntityBase } from 'vjcad';
// 删除实体
const entity: EntityBase = /* ... */;
Engine.currentDoc.currentSpace.erase([entity]);
// 记录删除操作(用于撤销时恢复)
Engine.undoManager.erased_undoMark([entity]);
// 或使用Engine封装方法
Engine.erasedUndoMark([entity]);
// 最简单的方式:使用eraseEntities自动记录
Engine.eraseEntities(entity); // 自动调用erased_undoMark修改实体
import { Engine, EntityBase } from 'vjcad';
const entity: EntityBase = /* ... */;
// 方式一:使用modEntity_undoMark
Engine.undoManager.modEntity_undoMark(entity); // 记录修改前状态
entity.color = 1; // 修改属性
entity.setModified();
// 方式二:使用Engine.markModified
Engine.markModified(entity); // 记录修改前状态
entity.layer = '新图层';
entity.setModified();
// 批量修改
Engine.markModifiedBatch([entity1, entity2, entity3]);移动实体
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const fromPoint = new Point2D(0, 0);
const toPoint = new Point2D(100, 100);
// 执行移动
for (const entity of entities) {
entity.move(fromPoint, toPoint);
}
// 记录移动操作
Engine.undoManager.moved_undoMark(entities, fromPoint, toPoint);
// 或使用Engine封装方法
Engine.movedUndoMark(entities, fromPoint, toPoint);旋转实体
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const basePoint = new Point2D(50, 50);
const angle = Math.PI / 4; // 45度
// 执行旋转
for (const entity of entities) {
entity.rotate(basePoint, angle);
}
// 记录旋转操作
Engine.undoManager.rotate_undoMark(entities, basePoint, angle);
// 或使用Engine封装方法
Engine.rotateUndoMark(entities, basePoint, angle);缩放实体
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const basePoint = new Point2D(50, 50);
const scale = 2.0;
// 执行缩放
for (const entity of entities) {
entity.scale(basePoint, scale);
}
// 记录缩放操作
Engine.undoManager.scaled_undoMark(entities, basePoint, scale);
// 或使用Engine封装方法
Engine.scaledUndoMark(entities, basePoint, scale);镜像实体
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const point1 = new Point2D(0, 0);
const point2 = new Point2D(100, 0); // 水平镜像线
// 执行镜像
for (const entity of entities) {
entity.mirror(point1, point2);
}
// 记录镜像操作
Engine.undoManager.mirrored_undoMark(entities, point1, point2);
// 或使用Engine封装方法
Engine.mirroredUndoMark(entities, point1, point2);核心方法
| 方法 | 说明 |
|---|---|
canUndo() | 检查是否可以撤销 |
canRedo() | 检查是否可以重做 |
undo() | 执行撤销操作 |
redo() | 执行重做操作 |
start_undoMark(description?) | 开始撤销标记组,可附加操作描述(description 类型:`string |
end_undoMark() | 结束撤销标记组 |
getUndoHistory() | 获取撤销历史列表(返回UndoHistoryItem[]) |
getRedoHistory() | 获取重做历史列表(返回UndoHistoryItem[]) |
getUndoCount() | 获取可撤销步数 |
getRedoCount() | 获取可重做步数 |
undoSteps(countOrName, options?) | 按步数或名称撤销 |
redoSteps(countOrName, options?) | 按步数或名称重做 |
rollbackLastGroup() | 静默回滚最后一组操作 |
撤销记录类型汇总
| 方法 | 说明 | 撤销时行为 |
|---|---|---|
added_undoMark(entities) | 记录添加实体 | 删除这些实体 |
erased_undoMark(entities) | 记录删除实体 | 恢复这些实体 |
modEntity_undoMark(entity) | 记录实体修改 | 恢复原始状态 |
moved_undoMark(entities, from, to) | 记录移动 | 反向移动 |
rotate_undoMark(entities, base, angle) | 记录旋转 | 反向旋转 |
scaled_undoMark(entities, base, scale) | 记录缩放 | 反向缩放 |
mirrored_undoMark(entities, p1, p2) | 记录镜像 | 再次镜像 |
entLayer_undoMark(entities) | 记录图层修改 | 恢复原图层 |
entColorIndex_undoMark(entities) | 记录颜色修改 | 恢复原颜色 |
entLineType_undoMark(entities) | 记录线型修改 | 恢复原线型 |
entLTScale_undoMark(entities) | 记录线型比例 | 恢复原比例 |
entTransp_undoMark(entities) | 记录透明度 | 恢复原透明度 |
clayer_undoMark(oldLayer) | 记录当前图层切换 | 恢复原图层 |
layerModified_undoMark(layer) | 记录图层属性修改 | 恢复原属性 |
撤销标记组
对于复杂操作(如一个命令中执行多个步骤),使用撤销标记组将多个操作合并为一个撤销单元。
startUndoRecord vs start_undoMark
Engine.startUndoRecord | undoMgr.start_undoMark | |
|---|---|---|
| 嵌套调用 | 安全 — 内部引用计数,仅最外层生效 | 不安全 — 每次调用都push START标记 |
| 适用场景 | 命令中(命令可能互相调用,产生嵌套) | 确定不会嵌套的UI代码(属性面板、对话框) |
| 建议 | 不确定时优先使用 | 底层控制或性能敏感场景 |
基本用法
import { Engine, LineEnt, CircleEnt } from 'vjcad';
const undoMgr = Engine.undoManager;
// 开始撤销组(可传入操作描述)
undoMgr.start_undoMark('创建组合图形');
try {
const line = new LineEnt([0, 0], [100, 0]);
line.setDefaults();
line.color = 1;
Engine.addEntities(line);
const circle = new CircleEnt([50, 50], 30);
circle.setDefaults();
circle.color = 3;
Engine.addEntities(circle);
} finally {
undoMgr.end_undoMark();
}
// 撤销时,line和circle会一起被删除,命令行提示"已撤销。创建组合图形"使用Engine封装方法
import { Engine } from 'vjcad';
// 开始撤销记录(支持嵌套,推荐在命令中使用)
Engine.startUndoRecord('批量修改');
try {
Engine.addEntities([entity1, entity2]);
Engine.setEntityColor([entity1, entity2], 1);
} finally {
Engine.endUndoRecord();
}嵌套撤销组
import { Engine } from 'vjcad';
// 支持嵌套调用
Engine.startUndoRecord('复合操作');
Engine.addEntities(entity1);
Engine.startUndoRecord(); // 嵌套(内层描述被忽略)
Engine.addEntities(entity2);
Engine.endUndoRecord();
Engine.addEntities(entity3);
Engine.endUndoRecord();
// 只有最外层的endUndoRecord会真正创建撤销组
// 所有操作(entity1, entity2, entity3)合并为一个撤销步骤检查嵌套状态
import { Engine } from 'vjcad';
if (Engine.isInUndoRecord()) {
console.log('当前在撤销组内');
}操作描述
撤销系统支持为每个可撤销操作附加描述信息(UndoDescriptor),描述信息会显示在:
- 撤销/重做按钮的tooltip(如"撤销: 修改颜色")
- 撤销/重做按钮下拉历史列表
- 命令行消息(如"已撤销。修改颜色")
UndoDescriptor
import { UndoDescriptor } from 'vjcad';
interface UndoDescriptor {
/** 国际化key,用于多语言翻译 */
i18nKey?: string;
/** 国际化参数(预留) */
params?: Record<string, string | number>;
/** 已解析的文本(直接显示,优先级高于i18nKey) */
resolvedText?: string;
/** 操作分类:edit=编辑操作, view=视图操作, system=系统操作 */
category: UndoCategory;
/** 来源命令名,如 'LINE', 'MOVE' */
sourceCommand?: string;
}
type UndoCategory = 'edit' | 'view' | 'system';字符串简写: API 也接受纯字符串作为简写,等价于 { category: 'edit', resolvedText: str }。适用于 start_undoMark 和 startUndoRecord。
描述解析优先级
显示label时的解析顺序:
resolvedText— 直接使用(最高优先级)i18nKey— 通过t()翻译后使用sourceCommand— 命令名兜底- 通用文案 — "撤销"/"重做"
使用方式
// 方式1:直接文本
Engine.startUndoRecord('批量修改颜色');
// 方式2:i18n key(推荐,支持多语言)
Engine.startUndoRecord({ category: 'edit', i18nKey: 'undo.modify.color' });
// 方式3:指定来源命令
Engine.startUndoRecord({ category: 'edit', sourceCommand: 'MYCMD' });
// 方式4:不传描述 — 在命令上下文中会自动推导
Engine.startUndoRecord();自动描述推导
当start_undoMark()不传description时,系统会尝试自动推导:
- 检查
Engine.editor.currentCommandName - 从
CommandRegistry查找命令定义 - 使用命令的
description或name作为resolvedText
适用场景:通过Editor.runCommand()执行的命令会自动设置命令名,因此无需手动传description。
不适用场景:属性面板、对话框等直接调用startUndoMark()的代码(不在命令上下文中),必须手动传入description。
操作分类
| category | 用途 | UI展示 |
|---|---|---|
edit | 编辑操作(绘图、修改属性等) | 历史面板主列表 |
view | 视图操作(缩放、平移、UCS等) | 历史面板折叠组 |
system | 系统操作(预留) | — |
查询撤销历史
import { Engine, UndoHistoryItem } from 'vjcad';
const undoMgr = Engine.undoManager;
// 获取撤销历史列表(最近操作在前)
const undoHistory: UndoHistoryItem[] = undoMgr.getUndoHistory();
// 获取重做历史列表
const redoHistory: UndoHistoryItem[] = undoMgr.getRedoHistory();
// 获取步数
const undoCount: number = undoMgr.getUndoCount();
const redoCount: number = undoMgr.getRedoCount();
// 最后一次撤销/重做的描述
const lastUndo = undoMgr.lastUndoDescription;
const lastRedo = undoMgr.lastRedoDescription;UndoHistoryItem
interface UndoHistoryItem {
/** 操作描述 */
description?: UndoDescriptor;
/** 在历史栈中的索引 */
index: number;
/** 是否为分组操作(START/END包裹) */
isGroup: boolean;
}批量撤销/重做
const undoMgr = Engine.undoManager;
// 撤销3步
undoMgr.undoSteps(3);
// 按名称撤销 — 撤销到最近一次匹配的操作(含),无匹配则不执行
undoMgr.undoSteps('绘制直线');
// 静默撤销(不触发命令行消息,用于UI跳转)
undoMgr.undoSteps(3, { silent: true });
// 重做2步
undoMgr.redoSteps(2);
// 按名称重做
undoMgr.redoSteps('绘制直线');
// 静默回滚最后一组操作(用于对话框取消等场景)
undoMgr.rollbackLastGroup();撤销栈变更事件
当撤销/重做栈发生变化时,CadEvents.UndoStackChanged事件会被触发:
import { CadEventManager, CadEvents } from 'vjcad';
CadEventManager.getInstance().on(CadEvents.UndoStackChanged, (args) => {
console.log('撤销步数:', args.undoCount);
console.log('重做步数:', args.redoCount);
console.log('下一步撤销:', args.nextUndoLabel);
console.log('下一步重做:', args.nextRedoLabel);
});事件参数:
interface UndoStackChangedEventArgs {
document: any;
undoCount: number;
redoCount: number;
nextUndoLabel?: string; // 下一步撤销的描述文本(已翻译)
nextRedoLabel?: string; // 下一步重做的描述文本(已翻译)
}撤销流程图
完整命令示例
import {
Engine,
LineEnt,
Point2D,
PointInputOptions,
InputStatusEnum
} from 'vjcad';
export class DrawLineCommand {
async main(): Promise<void> {
const undoMgr = Engine.undoManager;
const lines: LineEnt[] = [];
// 开始撤销组,附加描述
undoMgr.start_undoMark({ category: 'edit', sourceCommand: 'LINE' });
try {
const opt1 = new PointInputOptions("指定起点:");
const result1 = await Engine.getPoint(opt1);
if (result1.status !== InputStatusEnum.OK) return;
let startPoint = result1.value as Point2D;
while (true) {
const opt2 = new PointInputOptions("指定下一点或 [放弃(U)]:");
opt2.basePoint = startPoint;
opt2.useBasePoint = true;
opt2.keywords = ["U"];
const result2 = await Engine.getPoint(opt2);
if (result2.status === InputStatusEnum.CANCEL) {
break;
}
if (result2.status === InputStatusEnum.KEYWORD) {
if (result2.stringResult === "U" && lines.length > 0) {
const lastLine = lines.pop()!;
Engine.currentDoc.currentSpace.erase([lastLine]);
undoMgr.erased_undoMark([lastLine]);
if (lines.length > 0) {
startPoint = lines[lines.length - 1].endPoint;
}
}
continue;
}
if (result2.status === InputStatusEnum.OK) {
const endPoint = result2.value as Point2D;
const line = new LineEnt(startPoint.clone(), endPoint.clone());
line.setDefaults();
Engine.pcanvas.addEntity(line);
undoMgr.added_undoMark([line]);
lines.push(line);
startPoint = endPoint;
}
}
} finally {
undoMgr.end_undoMark();
}
}
}最佳实践
1. 始终配对使用
// 好的做法:使用try-finally
undoMgr.start_undoMark('我的操作');
try {
// 操作...
} finally {
undoMgr.end_undoMark();
}
// 或使用Engine封装
Engine.startUndoRecord('我的操作');
try {
// 操作...
} finally {
Engine.endUndoRecord();
}2. 批量操作放在同一撤销组
Engine.startUndoRecord('批量添加');
try {
for (const entity of entities) {
Engine.addEntities(entity);
}
} finally {
Engine.endUndoRecord();
}3. 修改前记录状态
// 好的做法:修改前记录
Engine.markModified(entity); // 先记录
entity.color = 1; // 再修改
entity.setModified();
// 不好的做法:修改后记录(无法恢复原始值)
entity.color = 1;
Engine.markModified(entity); // 此时记录的是新值4. 使用封装方法简化代码
// 推荐:使用Engine封装方法
Engine.setEntityColor(entities, 1); // 自动记录撤销
Engine.setEntityLayer(entities, '图层1');
// 而不是手动操作
for (const entity of entities) {
Engine.markModified(entity);
entity.color = 1;
}5. 条件执行时注意撤销组
Engine.startUndoRecord('条件操作');
try {
if (someCondition) {
Engine.addEntities(entity);
}
// 即使没有执行任何操作,也要正确关闭撤销组
} finally {
Engine.endUndoRecord();
}6. 为操作添加描述
// 推荐:使用i18n key,支持多语言
Engine.startUndoRecord({ category: 'edit', i18nKey: 'undo.modify.color' });
// 简单场景:直接使用文本
Engine.startUndoRecord('修改颜色');
// 属性面板等非命令场景必须手动传入描述
startUndoMark({ category: 'edit', i18nKey: 'undo.modify.radius' });