最佳实践
大约 7 分钟
最佳实践
本章总结 WebCAD 命令开发中的最佳实践和常见错误,帮助你编写高质量的命令代码。
命令结构最佳实践
1. 清晰的方法组织
将命令逻辑拆分为清晰的方法:
// 好的写法:逻辑清晰,职责分明
export class GoodCommand {
async main(): Promise<void> {
ssSetFirst([]);
Engine.undoManager.start_undoMark();
try {
if (!await this.getInputs()) return;
this.executeOperation();
this.showResult();
} finally {
this.cleanup();
Engine.undoManager.end_undoMark();
}
}
private async getInputs(): Promise<boolean> { /* ... */ }
private executeOperation(): void { /* ... */ }
private showResult(): void { /* ... */ }
private cleanup(): void { /* ... */ }
}
// 不好的写法:所有逻辑堆在 main() 中
export class BadCommand {
async main(): Promise<void> {
// 几百行代码全部在这里...
}
}2. 状态变量初始化
在构造函数中初始化所有状态变量:
// 好的写法
export class GoodCommand {
private points: Point2D[] = [];
private selectedEntities: any[] = [];
private stepNumber: number = 1;
constructor() {
this.points = [];
this.selectedEntities = [];
this.stepNumber = 1;
}
}
// 不好的写法:依赖类型推断的默认值
export class BadCommand {
private points; // undefined!
private stepNumber; // undefined!
}3. 使用 try-finally 确保清理
// 好的写法
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
try {
// 命令逻辑...
} finally {
// 无论成功失败,都会执行清理
Engine.undoManager.end_undoMark();
Engine.pcanvas.clearHighLight();
Engine.clearPreview();
}
}
// 不好的写法:可能遗漏清理
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
// 如果这里抛出异常,下面的代码不会执行
await this.doSomething();
Engine.undoManager.end_undoMark(); // 可能不会执行!
}输入处理最佳实践
1. 始终检查返回状态
// 好的写法
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
// 成功获取点
processPoint(result.value);
} else if (result.status === InputStatusEnum.Keyword) {
// 处理关键字
handleKeyword(result.stringResult);
} else if (result.status === InputStatusEnum.Cancel) {
// 用户取消
return;
}
// 不好的写法:忽略状态检查
const result = await getPoint(options);
processPoint(result.value); // 危险!result.value 可能无效2. 处理所有可能的状态
// 好的写法:全面处理
const result = await getPoint(options);
switch (result.status) {
case InputStatusEnum.OK:
// 处理点
break;
case InputStatusEnum.Keyword:
// 处理关键字
break;
case InputStatusEnum.EnterOrSpace:
// 使用默认值或结束
break;
case InputStatusEnum.Cancel:
case InputStatusEnum.None:
case InputStatusEnum.Error:
// 退出命令
return;
}
// 不好的写法:只处理部分状态
if (result.status === InputStatusEnum.OK) {
// 只处理成功情况
}
// 其他情况被忽略,可能导致意外行为3. 提供有意义的提示
// 好的写法:清晰的提示,包含可用选项
const options = new PointInputOptions(
"指定下一点 [闭合(C)/撤销(U)/圆弧(A)] <完成>:"
);
options.keywords = ["C", "U", "A"];
// 不好的写法:模糊的提示
const options = new PointInputOptions("输入:");4. 关键字使用大写字母
// 好的写法:关键字简短、大写
options.keywords = ["C", "U", "A", "L"];
// 处理时不区分大小写
if (result.stringResult.toUpperCase() === "C") {
// ...
}
// 不好的写法:关键字过长或混合大小写
options.keywords = ["Close", "undo", "ARC"];撤销处理最佳实践
1. 撤销标记成对使用
// 好的写法
Engine.undoManager.start_undoMark();
try {
// 操作...
} finally {
Engine.undoManager.end_undoMark();
}
// 不好的写法:可能遗漏 end
Engine.undoManager.start_undoMark();
await doSomething(); // 如果抛出异常...
Engine.undoManager.end_undoMark(); // 不会执行!2. 批量操作使用单个标记组
// 好的写法:一次撤销回退所有
Engine.undoManager.start_undoMark();
for (const entity of entities) {
entity.move(from, to);
}
Engine.undoManager.moved_undoMark(entities, from, to);
Engine.undoManager.end_undoMark();
// 不好的写法:每个实体一个标记组
for (const entity of entities) {
Engine.undoManager.start_undoMark(); // 错误!
entity.move(from, to);
Engine.undoManager.moved_undoMark([entity], from, to);
Engine.undoManager.end_undoMark();
}3. 修改前记录
// 好的写法:修改前记录
Engine.undoManager.modEntity_undoMark(entity);
entity.color = newColor;
// 不好的写法:修改后记录(无法恢复)
entity.color = newColor;
Engine.undoManager.modEntity_undoMark(entity); // 太晚了!4. 记录所有变更
// 好的写法:记录所有操作
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();预览处理最佳实践
1. 始终清理预览
// 好的写法
try {
options.callback = (pt) => {
Engine.pcanvas.drawControl.previewGraphics.clear();
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
await getPoint(options);
} finally {
Engine.pcanvas.drawControl.previewGraphics.clear();
}
// 不好的写法:可能遗漏清理
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
await getPoint(options);
// 如果用户取消,预览可能还在!2. 回调中先清除再绘制
// 好的写法
options.callback = (canvasPoint: Point2D) => {
// 先清除
Engine.pcanvas.drawControl.previewGraphics.clear();
// 再绘制
const preview = createPreview(canvasPoint);
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
// 不好的写法:不清除旧预览
options.callback = (canvasPoint: Point2D) => {
const preview = createPreview(canvasPoint);
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
// 旧预览还在,导致重叠!
};3. 多实体预览只注册一次
// 好的写法
private previewRegistered = false;
async getTargetPoint(): Promise<void> {
const options = new PointInputOptions("指定目标:");
options.callback = (pt) => this.updatePreview(pt);
if (!this.previewRegistered) {
Engine.pcanvas.drawControl.drawPreviewEntities(this.entities);
this.previewRegistered = true;
}
await getPoint(options);
}
// 不好的写法:每次回调都注册
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntities(this.entities); // 重复!
Engine.pcanvas.drawControl.setPreviewPosition(pt);
};选择处理最佳实践
1. 选择后高亮
// 好的写法
const result = await getSelections();
if (result.status === InputStatusEnum.OK && result.value.length > 0) {
Engine.pcanvas.highLightEntities(result.value);
Engine.pcanvas.redraw();
// 继续操作...
}
// 不好的写法:不提供视觉反馈
const result = await getSelections();
// 直接操作,用户不知道选了什么2. 操作后清除高亮
// 好的写法
try {
Engine.pcanvas.highLightEntities(entities);
// 操作...
} finally {
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();
}3. 验证选择结果
// 好的写法
const result = await getSelections();
if (result.status !== InputStatusEnum.OK) {
return; // 用户取消
}
if (result.value.length === 0) {
writeMessage("<br/>未选择任何对象。");
return;
}
// 继续操作...好写法 vs 坏写法对比
对比1:命令入口
// 好的写法
export class GoodCommand {
async main(): Promise<void> {
ssSetFirst([]);
Engine.undoManager.start_undoMark();
try {
await this.execute();
} finally {
Engine.undoManager.end_undoMark();
this.cleanup();
}
}
private cleanup(): void {
Engine.pcanvas.clearHighLight();
Engine.clearPreview();
Engine.pcanvas.regen();
}
}
// 坏的写法
export class BadCommand {
async main(): Promise<void> {
// 没有清理选择集
// 没有撤销标记
await this.execute();
// 没有清理高亮和预览
}
}对比2:点拾取
// 好的写法
private async getNextPoint(): Promise<Point2D | null> {
const options = new PointInputOptions("指定下一点 [撤销(U)]:");
options.keywords = ["U"];
options.useBasePoint = true;
options.basePoint = this.lastPoint;
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
return result.value;
} else if (result.stringResult === "U") {
this.undoLastPoint();
return await this.getNextPoint(); // 递归重试
}
return null; // 取消或错误
}
// 坏的写法
private async getNextPoint(): Promise<Point2D> {
const result = await getPoint(new PointInputOptions("点:"));
return result.value; // 不检查状态,可能返回无效值
}对比3:实体创建
// 好的写法
private createEntity(): void {
const entity = new LineEnt(this.p1, this.p2);
entity.setDefaults(); // 使用当前图层、颜色等
Engine.pcanvas.addEntity(entity);
Engine.undoManager.added_undoMark([entity]);
writeMessage("<br/>直线已创建。");
}
// 坏的写法
private createEntity(): void {
const entity = new LineEnt(this.p1, this.p2);
// 没有设置默认属性
Engine.pcanvas.addEntity(entity);
// 没有记录撤销
// 没有用户反馈
}常见错误
错误1:忘记 end_undoMark
// 错误
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
const result = await getPoint(options);
if (result.status !== InputStatusEnum.OK) {
return; // 提前返回,end_undoMark 不会执行!
}
Engine.undoManager.end_undoMark();
}
// 正确
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
try {
const result = await getPoint(options);
if (result.status !== InputStatusEnum.OK) {
return;
}
// ...
} finally {
Engine.undoManager.end_undoMark(); // 总是执行
}
}错误2:忘记清理预览
// 错误
async main(): Promise<void> {
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
const result = await getPoint(options);
// 用户取消后,预览还在画布上!
}
// 正确
async main(): Promise<void> {
options.callback = (pt) => {
Engine.pcanvas.drawControl.previewGraphics.clear();
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
try {
const result = await getPoint(options);
} finally {
Engine.pcanvas.drawControl.previewGraphics.clear();
}
}错误3:未处理 Cancel 状态
// 错误
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
}
// Cancel 状态被忽略,命令可能继续执行错误的逻辑
// 正确
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
} else if (result.status === InputStatusEnum.Cancel) {
return; // 明确退出
}错误4:坐标系混淆
// 错误:回调中直接使用画布坐标
options.callback = (canvasPoint: Point2D) => {
const circle = new CircleEnt(this.center,
this.center.distanceTo(canvasPoint)); // 错误!canvasPoint 是画布坐标
};
// 正确:转换为世界坐标
options.callback = (canvasPoint: Point2D) => {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
const circle = new CircleEnt(this.center,
this.center.distanceTo(worldPoint)); // 正确!
};错误5:循环中的内存泄漏
// 错误:预览实体不断累积
while (true) {
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntities(entities); // 重复添加!
};
await getPoint(options);
}
// 正确:只注册一次,用标志控制
let registered = false;
while (true) {
options.callback = (pt) => {
if (!registered) {
Engine.pcanvas.drawControl.drawPreviewEntities(entities);
registered = true;
}
Engine.pcanvas.drawControl.setPreviewPosition(pt);
};
await getPoint(options);
// 每次循环后重置
Engine.pcanvas.drawControl.resetPreview();
registered = false;
}检查清单
在提交命令代码前,检查以下项目:
- [ ]
main()方法开始时调用ssSetFirst([]) - [ ] 撤销标记
start_undoMark()和end_undoMark()成对使用 - [ ] 使用 try-finally 确保清理代码执行
- [ ] 所有输入函数的返回状态都有检查
- [ ] Cancel 状态有明确处理
- [ ] 预览绑定后有清理逻辑
- [ ] 高亮实体后有清除逻辑
- [ ] 实体创建后调用
setDefaults() - [ ] 实体添加后记录撤销信息
- [ ] 提供有意义的用户提示
- [ ] 操作完成后刷新画布
regen()或redraw()