Best Practices
About 5 min
Best Practices
This chapter summarizes best practices and common mistakes in WebCAD command development to help you write high-quality command code.
Command Structure Best Practices
1. Clear Method Organization
Split command logic into clear methods:
// Good: Clear logic, well-defined responsibilities
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 { /* ... */ }
}
// Bad: All logic crammed into main()
export class BadCommand {
async main(): Promise<void> {
// Hundreds of lines of code all here...
}
}2. State Variable Initialization
Initialize all state variables in the constructor:
// Good
export class GoodCommand {
private points: Point2D[] = [];
private selectedEntities: any[] = [];
private stepNumber: number = 1;
constructor() {
this.points = [];
this.selectedEntities = [];
this.stepNumber = 1;
}
}
// Bad: Relying on type inference defaults
export class BadCommand {
private points; // undefined!
private stepNumber; // undefined!
}3. Use try-finally to Ensure Cleanup
// Good
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
try {
// Command logic...
} finally {
// Cleanup always executes regardless of success or failure
Engine.undoManager.end_undoMark();
Engine.pcanvas.clearHighLight();
Engine.clearPreview();
}
}
// Bad: Cleanup may be missed
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
// If an exception is thrown here, the code below won't execute
await this.doSomething();
Engine.undoManager.end_undoMark(); // May not execute!
}Input Handling Best Practices
1. Always Check Return Status
// Good
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
// Successfully got point
processPoint(result.value);
} else if (result.status === InputStatusEnum.Keyword) {
// Handle keyword
handleKeyword(result.stringResult);
} else if (result.status === InputStatusEnum.Cancel) {
// User cancelled
return;
}
// Bad: Ignoring status check
const result = await getPoint(options);
processPoint(result.value); // Dangerous! result.value may be invalid2. Handle All Possible Statuses
// Good: Comprehensive handling
const result = await getPoint(options);
switch (result.status) {
case InputStatusEnum.OK:
// Handle point
break;
case InputStatusEnum.Keyword:
// Handle keyword
break;
case InputStatusEnum.EnterOrSpace:
// Use default value or finish
break;
case InputStatusEnum.Cancel:
case InputStatusEnum.None:
case InputStatusEnum.Error:
// Exit command
return;
}
// Bad: Only handling some statuses
if (result.status === InputStatusEnum.OK) {
// Only handling success case
}
// Other cases ignored, may cause unexpected behavior3. Provide Meaningful Prompts
// Good: Clear prompt with available options
const options = new PointInputOptions(
"Specify next point [Close(C)/Undo(U)/Arc(A)] <Done>:"
);
options.keywords = ["C", "U", "A"];
// Bad: Vague prompt
const options = new PointInputOptions("Input:");4. Use Uppercase Letters for Keywords
// Good: Short, uppercase keywords
options.keywords = ["C", "U", "A", "L"];
// Case-insensitive handling
if (result.stringResult.toUpperCase() === "C") {
// ...
}
// Bad: Keywords too long or mixed case
options.keywords = ["Close", "undo", "ARC"];Undo Handling Best Practices
1. Use Undo Marks in Pairs
// Good
Engine.undoManager.start_undoMark();
try {
// Operations...
} finally {
Engine.undoManager.end_undoMark();
}
// Bad: end may be missed
Engine.undoManager.start_undoMark();
await doSomething(); // If exception thrown...
Engine.undoManager.end_undoMark(); // Won't execute!2. Use a Single Mark Group for Batch Operations
// Good: Single undo reverts all
Engine.undoManager.start_undoMark();
for (const entity of entities) {
entity.move(from, to);
}
Engine.undoManager.moved_undoMark(entities, from, to);
Engine.undoManager.end_undoMark();
// Bad: One mark group per entity
for (const entity of entities) {
Engine.undoManager.start_undoMark(); // Wrong!
entity.move(from, to);
Engine.undoManager.moved_undoMark([entity], from, to);
Engine.undoManager.end_undoMark();
}3. Record Before Modifying
// Good: Record before modification
Engine.undoManager.modEntity_undoMark(entity);
entity.color = newColor;
// Bad: Recording after modification (cannot restore)
entity.color = newColor;
Engine.undoManager.modEntity_undoMark(entity); // Too late!4. Record All Changes
// Good: Record all operations
Engine.undoManager.start_undoMark();
// Add new entity
Engine.pcanvas.addEntity(newEntity);
Engine.undoManager.added_undoMark([newEntity]);
// Delete old entity
Engine.currentSpace.erase([oldEntity]);
Engine.undoManager.erased_undoMark([oldEntity]);
Engine.undoManager.end_undoMark();
// Bad: Missing operation records
Engine.undoManager.start_undoMark();
Engine.pcanvas.addEntity(newEntity);
Engine.undoManager.added_undoMark([newEntity]);
Engine.currentSpace.erase([oldEntity]);
// Forgot to record deletion! Undo will have issues
Engine.undoManager.end_undoMark();Preview Handling Best Practices
1. Always Clean Up Previews
// Good
try {
options.callback = (pt) => {
Engine.pcanvas.drawControl.previewGraphics.clear();
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
await getPoint(options);
} finally {
Engine.pcanvas.drawControl.previewGraphics.clear();
}
// Bad: Cleanup may be missed
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
await getPoint(options);
// If user cancels, preview may still be on screen!2. Clear Before Draw in Callbacks
// Good
options.callback = (canvasPoint: Point2D) => {
// Clear first
Engine.pcanvas.drawControl.previewGraphics.clear();
// Then draw
const preview = createPreview(canvasPoint);
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
// Bad: Not clearing old preview
options.callback = (canvasPoint: Point2D) => {
const preview = createPreview(canvasPoint);
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
// Old preview still there, causing overlap!
};3. Register Multi-entity Preview Only Once
// Good
private previewRegistered = false;
async getTargetPoint(): Promise<void> {
const options = new PointInputOptions("Specify target:");
options.callback = (pt) => this.updatePreview(pt);
if (!this.previewRegistered) {
Engine.pcanvas.drawControl.drawPreviewEntities(this.entities);
this.previewRegistered = true;
}
await getPoint(options);
}
// Bad: Registering on every callback
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntities(this.entities); // Duplicate!
Engine.pcanvas.drawControl.setPreviewPosition(pt);
};Selection Handling Best Practices
1. Highlight After Selection
// Good
const result = await getSelections();
if (result.status === InputStatusEnum.OK && result.value.length > 0) {
Engine.pcanvas.highLightEntities(result.value);
Engine.pcanvas.redraw();
// Continue operations...
}
// Bad: No visual feedback
const result = await getSelections();
// Operating directly, user doesn't know what's selected2. Clear Highlight After Operation
// Good
try {
Engine.pcanvas.highLightEntities(entities);
// Operations...
} finally {
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();
}3. Validate Selection Results
// Good
const result = await getSelections();
if (result.status !== InputStatusEnum.OK) {
return; // User cancelled
}
if (result.value.length === 0) {
writeMessage("<br/>No objects selected.");
return;
}
// Continue operations...Good vs Bad Comparison
Comparison 1: Command Entry
// Good
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();
}
}
// Bad
export class BadCommand {
async main(): Promise<void> {
// No selection set cleanup
// No undo marks
await this.execute();
// No highlight and preview cleanup
}
}Comparison 2: Point Picking
// Good
private async getNextPoint(): Promise<Point2D | null> {
const options = new PointInputOptions("Specify next point [Undo(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(); // Recursive retry
}
return null; // Cancel or error
}
// Bad
private async getNextPoint(): Promise<Point2D> {
const result = await getPoint(new PointInputOptions("Point:"));
return result.value; // No status check, may return invalid value
}Comparison 3: Entity Creation
// Good
private createEntity(): void {
const entity = new LineEnt(this.p1, this.p2);
entity.setDefaults(); // Use current layer, color, etc.
Engine.pcanvas.addEntity(entity);
Engine.undoManager.added_undoMark([entity]);
writeMessage("<br/>Line created.");
}
// Bad
private createEntity(): void {
const entity = new LineEnt(this.p1, this.p2);
// No default properties set
Engine.pcanvas.addEntity(entity);
// No undo record
// No user feedback
}Common Mistakes
Mistake 1: Forgetting end_undoMark
// Wrong
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
const result = await getPoint(options);
if (result.status !== InputStatusEnum.OK) {
return; // Early return, end_undoMark won't execute!
}
Engine.undoManager.end_undoMark();
}
// Correct
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(); // Always executes
}
}Mistake 2: Forgetting to Clean Up Previews
// Wrong
async main(): Promise<void> {
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
const result = await getPoint(options);
// If user cancels, preview is still on the canvas!
}
// Correct
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();
}
}Mistake 3: Not Handling Cancel Status
// Wrong
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
}
// Cancel status ignored, command may continue with incorrect logic
// Correct
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
} else if (result.status === InputStatusEnum.Cancel) {
return; // Explicitly exit
}Mistake 4: Coordinate System Confusion
// Wrong: Using canvas coordinates directly in callback
options.callback = (canvasPoint: Point2D) => {
const circle = new CircleEnt(this.center,
this.center.distanceTo(canvasPoint)); // Wrong! canvasPoint is in canvas coordinates
};
// Correct: Convert to world coordinates
options.callback = (canvasPoint: Point2D) => {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
const circle = new CircleEnt(this.center,
this.center.distanceTo(worldPoint)); // Correct!
};Mistake 5: Memory Leak in Loops
// Wrong: Preview entities keep accumulating
while (true) {
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntities(entities); // Repeatedly added!
};
await getPoint(options);
}
// Correct: Register only once, use flag to control
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);
// Reset after each loop
Engine.pcanvas.drawControl.resetPreview();
registered = false;
}Checklist
Before submitting command code, check the following items:
- [ ]
main()method callsssSetFirst([])at the beginning - [ ] Undo marks
start_undoMark()andend_undoMark()are used in pairs - [ ] Use try-finally to ensure cleanup code executes
- [ ] Return status of all input functions is checked
- [ ] Cancel status is explicitly handled
- [ ] Preview has cleanup logic after binding
- [ ] Highlighted entities have clearing logic
- [ ]
setDefaults()is called after entity creation - [ ] Undo information is recorded after entity addition
- [ ] Meaningful user prompts are provided
- [ ] Canvas is refreshed with
regen()orredraw()after operations
Next Steps
- API Reference - Complete API reference
- Command Design Patterns - Command design patterns