撤销重做机制
大约 6 分钟
撤销重做机制
WebCAD 提供完善的撤销/重做机制,确保用户可以回退和恢复操作。本章详细介绍如何在命令中正确实现撤销支持。
核心概念
撤销标记组
撤销标记组(Undo Mark)将多个操作组合为一个可撤销的单元。一个命令中的所有操作应该在同一个标记组内,这样用户执行一次撤销就能回退整个命令。
// 开始撤销标记组
Engine.undoManager.start_undoMark();
try {
// 命令操作...
// 所有在此范围内的操作将作为一个整体被撤销
} finally {
// 结束撤销标记组
Engine.undoManager.end_undoMark();
}撤销记录
在标记组内,需要记录每个具体操作,以便撤销时能正确恢复。
基本用法
添加实体
当命令创建新实体时,记录添加操作:
import { Engine, LineEnt, Point2D } from 'vjcad';
// 创建实体
const line = new LineEnt(
new Point2D(0, 0),
new Point2D(100, 100)
);
line.setDefaults();
// 添加到画布
Engine.pcanvas.addEntity(line);
// 记录撤销信息
Engine.undoManager.added_undoMark([line]);删除实体
当命令删除实体时,记录删除操作:
// 获取要删除的实体
const entities = [...selectedEntities];
// 执行删除
Engine.currentDoc.currentSpace.erase(entities);
// 记录撤销信息
Engine.undoManager.erased_undoMark(entities);移动实体
当命令移动实体时,记录移动操作:
const fromPoint = new Point2D(0, 0);
const toPoint = new Point2D(100, 50);
// 执行移动
for (const entity of entities) {
entity.move(fromPoint, toPoint);
}
// 记录撤销信息
Engine.undoManager.moved_undoMark(entities, fromPoint, toPoint);旋转实体
当命令旋转实体时,记录旋转操作:
const center = new Point2D(50, 50);
const angle = Math.PI / 4; // 45度
// 执行旋转
for (const entity of entities) {
entity.rotate(center, angle);
}
// 记录撤销信息
Engine.undoManager.rotate_undoMark(entities, center, angle);镜像实体
当命令镜像实体时,记录镜像操作:
const mirrorPoint1 = new Point2D(0, 0);
const mirrorPoint2 = new Point2D(0, 100); // 垂直镜像线
// 执行镜像
for (const entity of entities) {
entity.mirror(mirrorPoint1, mirrorPoint2);
}
// 记录撤销信息
Engine.undoManager.mirrored_undoMark(entities, mirrorPoint1, mirrorPoint2);修改实体属性
当命令修改实体属性时,需要在修改前记录:
// 在修改前记录原状态
Engine.undoManager.modEntity_undoMark(entity);
// 然后修改实体
entity.color = 1; // 红色
entity.layer = "新图层";撤销 API 参考
UndoManager 核心方法
| 方法 | 参数 | 说明 |
|---|---|---|
canUndo() | - | 检查是否可以撤销 |
canRedo() | - | 检查是否可以重做 |
undo() | - | 执行撤销操作 |
redo() | - | 执行重做操作 |
start_undoMark() | - | 开始撤销标记组 |
end_undoMark() | - | 结束撤销标记组 |
UndoManager 记录方法
| 方法 | 参数 | 说明 |
|---|---|---|
added_undoMark(entities) | EntityBase[] | 记录添加实体 |
erased_undoMark(entities) | EntityBase[] | 记录删除实体 |
moved_undoMark(entities, from, to) | EntityBase[], Point2D, Point2D | 记录移动 |
rotate_undoMark(entities, center, angle) | EntityBase[], Point2D, number | 记录旋转 |
mirrored_undoMark(entities, pt1, pt2) | EntityBase[], Point2D, Point2D | 记录镜像 |
modEntity_undoMark(entity) | EntityBase | 记录实体修改(修改前调用) |
modLine_undoMark(line) | LineEnt | 记录直线修改 |
modArc_undoMark(arc) | ArcEnt | 记录圆弧修改 |
modRay_undoMark(ray) | RayEnt | 记录射线修改 |
完整示例
示例1:绘制直线命令
import { Engine, LineEnt, Point2D } from 'vjcad';
import {
getPoint,
PointInputOptions,
InputStatusEnum,
ssSetFirst
} from 'vjcad';
export class LineCommand {
private points: Point2D[] = [];
async main(): Promise<void> {
ssSetFirst([]);
// 开始撤销标记组
Engine.undoManager.start_undoMark();
try {
// 获取第一点
const p1Result = await getPoint(new PointInputOptions("指定第一点:"));
if (p1Result.status !== InputStatusEnum.OK) return;
this.points.push(p1Result.value);
// 连续获取更多点
while (true) {
const options = new PointInputOptions("指定下一点 [撤销(U)]:");
options.keywords = ["U"];
options.useBasePoint = true;
options.basePoint = this.points[this.points.length - 1];
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
const newPoint = result.value;
const lastPoint = this.points[this.points.length - 1];
// 创建直线
const line = new LineEnt(lastPoint, newPoint);
line.setDefaults();
Engine.pcanvas.addEntity(line);
// 记录撤销
Engine.undoManager.added_undoMark([line]);
this.points.push(newPoint);
} else if (result.stringResult === "U") {
// 撤销最后一条线
if (this.points.length > 1) {
const lastEntity = Engine.currentDoc.currentSpace.items.pop();
Engine.undoManager.items.pop();
this.points.pop();
Engine.pcanvas.regen();
}
} else {
break;
}
}
} finally {
// 确保结束撤销标记组
Engine.undoManager.end_undoMark();
}
}
}示例2:复制命令
import { Engine, Point2D } from 'vjcad';
import {
getSelections,
getPoint,
SelectionInputOptions,
PointInputOptions,
InputStatusEnum
} from 'vjcad';
export class CopyCommand {
private selectedEntities: any[] = [];
private basePoint: Point2D = new Point2D();
async main(): Promise<void> {
// 选择实体
const selResult = await getSelections(new SelectionInputOptions());
if (selResult.status !== InputStatusEnum.OK || selResult.value.length === 0) {
return;
}
this.selectedEntities = selResult.value;
// 获取基点
const baseResult = await getPoint(new PointInputOptions("指定基点:"));
if (baseResult.status !== InputStatusEnum.OK) return;
this.basePoint = baseResult.value;
// 连续复制
while (true) {
const targetOptions = new PointInputOptions("指定复制目标点 <完成>:");
targetOptions.useBasePoint = true;
targetOptions.basePoint = this.basePoint;
const targetResult = await getPoint(targetOptions);
if (targetResult.status === InputStatusEnum.OK) {
this.executeCopy(targetResult.value);
} else {
break;
}
}
Engine.pcanvas.regen();
}
private executeCopy(targetPoint: Point2D): void {
// 注意:每次复制作为独立的撤销单元
Engine.undoManager.start_undoMark();
const copiedEntities: any[] = [];
for (const entity of this.selectedEntities) {
// 克隆实体
const cloned = entity.clone();
// 移动到目标位置
cloned.move(this.basePoint, targetPoint);
// 添加到画布
Engine.pcanvas.addEntity(cloned);
copiedEntities.push(cloned);
}
// 记录撤销
Engine.undoManager.added_undoMark(copiedEntities);
Engine.undoManager.end_undoMark();
}
}示例3:修改实体属性
import { Engine } from 'vjcad';
import {
getSelections,
InputStatusEnum,
writeMessage
} from 'vjcad';
export class ChangeColorCommand {
async main(): Promise<void> {
// 选择实体
const result = await getSelections();
if (result.status !== InputStatusEnum.OK || result.value.length === 0) {
return;
}
const entities = result.value;
const newColor = 1; // 红色
Engine.undoManager.start_undoMark();
try {
for (const entity of entities) {
// 在修改前记录原状态
Engine.undoManager.modEntity_undoMark(entity);
// 修改颜色
entity.color = newColor;
}
Engine.pcanvas.regen();
writeMessage(`<br/>已修改 ${entities.length} 个实体的颜色。`);
} finally {
Engine.undoManager.end_undoMark();
}
}
}示例4:修剪直线
import { Engine, LineEnt, Point2D } from 'vjcad';
function trimLineAtPoint(line: LineEnt, trimPoint: Point2D): void {
Engine.undoManager.start_undoMark();
try {
// 记录修改前状态
Engine.undoManager.modLine_undoMark(line);
// 修改直线端点
line.startPoint = trimPoint;
Engine.pcanvas.regen();
} finally {
Engine.undoManager.end_undoMark();
}
}示例5:复杂变换(移动+旋转+镜像)
import { Engine, Point2D } from 'vjcad';
import { startUndoMark, endUndoMark } from 'vjcad';
function complexTransform(
entities: any[],
basePoint: Point2D,
targetPoint: Point2D,
rotationAngle: number,
mirrorX: boolean,
mirrorY: boolean
): void {
// 使用辅助函数
startUndoMark();
try {
const verticalMirrorLine = new Point2D(basePoint.x, basePoint.y + 1);
const horizontalMirrorLine = new Point2D(basePoint.x + 1, basePoint.y);
// 执行变换
for (const entity of entities) {
if (mirrorX) {
entity.mirror(basePoint, verticalMirrorLine);
}
if (mirrorY) {
entity.mirror(basePoint, horizontalMirrorLine);
}
if (rotationAngle !== 0) {
entity.rotate(basePoint, rotationAngle);
}
entity.move(basePoint, targetPoint);
}
// 按顺序记录撤销(与执行顺序一致)
if (mirrorX) {
Engine.undoManager.mirrored_undoMark(entities, basePoint, verticalMirrorLine);
}
if (mirrorY) {
Engine.undoManager.mirrored_undoMark(entities, basePoint, horizontalMirrorLine);
}
if (rotationAngle !== 0) {
Engine.undoManager.rotate_undoMark(entities, basePoint, rotationAngle);
}
Engine.undoManager.moved_undoMark(entities, basePoint, targetPoint);
} finally {
endUndoMark();
}
Engine.pcanvas.regen();
}最佳实践
1. 使用 try-finally 确保结束标记
Engine.undoManager.start_undoMark();
try {
// 可能抛出异常的操作
await riskyOperation();
} finally {
// 无论成功或失败,都要结束标记
Engine.undoManager.end_undoMark();
}2. 批量操作使用单个标记组
// 好的写法:所有操作在一个标记组内
Engine.undoManager.start_undoMark();
for (const entity of entities) {
entity.move(fromPoint, toPoint);
}
Engine.undoManager.moved_undoMark(entities, fromPoint, toPoint);
Engine.undoManager.end_undoMark();
// 不好的写法:每个实体一个标记组
for (const entity of entities) {
Engine.undoManager.start_undoMark(); // 不要这样!
entity.move(fromPoint, toPoint);
Engine.undoManager.moved_undoMark([entity], fromPoint, toPoint);
Engine.undoManager.end_undoMark();
}3. 记录所有变更
// 好的写法:记录所有操作
Engine.undoManager.start_undoMark();
// 添加新实体
Engine.pcanvas.addEntity(newEntity);
Engine.undoManager.added_undoMark([newEntity]);
// 删除旧实体
Engine.currentSpace.erase([oldEntity]);
Engine.undoManager.erased_undoMark([oldEntity]);
Engine.undoManager.end_undoMark();
// 不好的写法:遗漏删除操作的记录
Engine.undoManager.start_undoMark();
Engine.pcanvas.addEntity(newEntity);
Engine.undoManager.added_undoMark([newEntity]);
Engine.currentSpace.erase([oldEntity]);
// 忘记记录!撤销时会出问题
Engine.undoManager.end_undoMark();4. 修改属性前记录
// 正确:修改前记录
Engine.undoManager.modEntity_undoMark(entity);
entity.color = newColor;
// 错误:修改后记录(无法恢复原值)
entity.color = newColor;
Engine.undoManager.modEntity_undoMark(entity); // 太晚了!5. 标记组要成对
// 正确:成对使用
Engine.undoManager.start_undoMark();
// ...
Engine.undoManager.end_undoMark();
// 错误:只开始不结束
Engine.undoManager.start_undoMark();
// ...
// 忘记 end_undoMark()!后续命令会出问题常见问题
Q: 为什么撤销不起作用?
检查以下几点:
- 是否调用了
start_undoMark()和end_undoMark() - 是否正确调用了对应的记录方法(如
added_undoMark) - 记录方法的参数是否正确
Q: 如何实现命令内的撤销(如直线命令的 U 选项)?
命令内撤销需要手动管理:
if (result.stringResult === "U") {
// 移除最后添加的实体
Engine.currentDoc.currentSpace.items.pop();
// 移除对应的撤销记录
Engine.undoManager.items.pop();
// 刷新显示
Engine.pcanvas.regen();
}Q: 连续操作如何组织撤销组?
取决于用户预期:
- 如果用户期望一次撤销回退整个命令,使用单个标记组
- 如果用户期望分步撤销,每步使用独立标记组