Command Design Patterns
About 6 min
Command Design Patterns
Different types of commands have different interaction patterns. This chapter introduces common command design patterns in WebCAD to help you choose the right implementation approach.
Pattern Overview
| Pattern | Characteristics | Use Cases | Examples |
|---|---|---|---|
| Simple Command | One-shot execution, no interaction | Batch operations, system commands | ERASE, REGEN |
| Step-based Command | P1->P2->P3 method chain | Drawing commands with fixed steps | LINE, CIRCLE |
| State Machine Command | while + switch | Commands with complex branching | Multi-option commands |
| Loop Interaction Command | while(true) loop | Continuous operations | TRIM, OFFSET |
| Mode Switching Command | Single/continuous toggle | Optional continuous operations | COPY |
Pattern 1: Simple Command
The simplest command pattern — executes immediately upon invocation, requiring no user interaction.
Characteristics
- One-shot execution
- No user interaction or only entity selection
- Suitable for batch operations
Example: Erase Command
import { Engine } from 'vjcad';
import { getSelections, InputStatusEnum, writeMessage } from 'vjcad';
export class EraseCommand {
async main(): Promise<void> {
// Select entities
const result = await getSelections();
if (result.status === InputStatusEnum.OK && result.value.length > 0) {
const entities = result.value;
// Execute deletion
Engine.currentDoc.currentSpace.erase(entities);
Engine.undoManager.erased_undoMark(entities);
// Refresh
Engine.pcanvas.clearGrip();
Engine.pcanvas.regen();
writeMessage(`<br/>Deleted ${entities.length} entities.`);
}
}
}Example: Zoom Extents Command
import { Engine } from 'vjcad';
export class ZoomExtentsCommand {
async main(): Promise<void> {
Engine.pcanvas.zoomExtents();
Engine.pcanvas.regen();
}
}Example: Regen Command
import { Engine } from 'vjcad';
export class RegenCommand {
async main(): Promise<void> {
Engine.pcanvas.regen();
}
}Pattern 2: Step-based Command
Implements a fixed-step command flow through P1(), P2(), P3() methods.
Characteristics
- Clear step division
- Uses stepNumber to control flow
- Supports undo to previous step
Example: Line Command
import { Point2D, LineEnt, Engine } from 'vjcad';
import {
getPoint,
PointInputOptions,
InputStatusEnum,
ssSetFirst,
writeMessage
} from 'vjcad';
export class LineCommand {
private points: Point2D[] = [];
private stepNumber: number = 1;
private startPoint: Point2D = new Point2D();
async main(): Promise<void> {
ssSetFirst([]);
Engine.undoManager.start_undoMark();
// Step loop
while (this.stepNumber > 0) {
switch (this.stepNumber) {
case 1:
await this.P1();
break;
case 2:
await this.P2();
break;
default:
await this.P3();
break;
}
}
Engine.undoManager.end_undoMark();
}
// Step 1: Get start point
private async P1(): Promise<void> {
const options = new PointInputOptions("Specify first point:");
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.startPoint = result.value;
this.points.push(result.value);
this.stepNumber = 2;
} else {
this.stepNumber = 0; // Exit
}
}
// Step 2: Get second point
private async P2(): Promise<void> {
const lastPoint = this.points[this.points.length - 1];
const options = new PointInputOptions("Specify second point [Undo(U)]:");
options.keywords = ["U"];
options.useBasePoint = true;
options.basePoint = lastPoint;
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.createLine(lastPoint, result.value);
this.points.push(result.value);
this.stepNumber = 3;
} else if (result.stringResult === "U") {
this.points.pop();
this.stepNumber = 1;
} else {
this.stepNumber = 0;
}
}
// Step 3 and beyond: Continue drawing
private async P3(): Promise<void> {
const lastPoint = this.points[this.points.length - 1];
const options = new PointInputOptions(
`Specify point ${this.stepNumber} [Close(C)/Undo(U)]:`
);
options.keywords = ["C", "U"];
options.useBasePoint = true;
options.basePoint = lastPoint;
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.createLine(lastPoint, result.value);
this.points.push(result.value);
this.stepNumber++;
} else if (result.stringResult === "C") {
// Close
if (this.points.length >= 2) {
this.createLine(lastPoint, this.startPoint);
}
this.stepNumber = 0;
} else if (result.stringResult === "U") {
// Undo
if (this.points.length > 1) {
this.points.pop();
Engine.currentDoc.currentSpace.items.pop();
Engine.undoManager.items.pop();
Engine.pcanvas.regen();
this.stepNumber--;
}
} else {
this.stepNumber = 0;
}
}
private createLine(p1: Point2D, p2: Point2D): void {
const line = new LineEnt(p1, p2);
line.setDefaults();
Engine.pcanvas.addEntity(line);
Engine.undoManager.added_undoMark([line]);
}
}Example: Circle Command
import { Point2D, CircleEnt, Engine } from 'vjcad';
import {
getPoint,
PointInputOptions,
InputStatusEnum,
ssSetFirst,
writeMessage
} from 'vjcad';
export class CircleCommand {
private static lastRadius: number = 10;
private center: Point2D = new Point2D();
private radius: number = 0;
async main(): Promise<void> {
ssSetFirst([]);
await this.P1();
}
// Step 1: Get center point
private async P1(): Promise<void> {
const options = new PointInputOptions("Specify center:");
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.center = result.value;
await this.P2();
}
}
// Step 2: Get radius
private async P2(): Promise<void> {
const options = new PointInputOptions(
`Specify radius <${CircleCommand.lastRadius}>:`
);
options.useBasePoint = true;
options.basePoint = this.center;
// Preview callback
options.callback = (canvasPoint: Point2D) => {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
const radius = this.center.distanceTo(worldPoint);
const preview = new CircleEnt(this.center, radius);
preview.setDefaults();
Engine.pcanvas.drawControl.previewGraphics.clear();
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
const result = await getPoint(options);
Engine.pcanvas.drawControl.previewGraphics.clear();
if (result.status === InputStatusEnum.OK) {
this.radius = this.center.distanceTo(result.value);
this.createCircle();
} else if (result.status === InputStatusEnum.EnterOrSpace) {
// Use default radius
this.radius = CircleCommand.lastRadius;
this.createCircle();
}
}
private createCircle(): void {
const circle = new CircleEnt(this.center, this.radius);
circle.setDefaults();
Engine.pcanvas.addEntity(circle);
Engine.undoManager.added_undoMark([circle]);
CircleCommand.lastRadius = this.radius;
writeMessage("<br/>Circle created.");
}
}Pattern 3: State Machine Command
Uses a while loop and switch statement to implement complex branching logic.
Characteristics
- Flexible state transitions
- Suitable for commands with multiple branches and options
- States can jump to any step
Example: Multi-step Command
import { Point2D, PolylineEnt, BulgePoint, Engine } from 'vjcad';
import {
getPoint,
PointInputOptions,
InputStatusEnum,
writeMessage
} from 'vjcad';
enum CommandState {
GetFirstPoint = 1,
GetNextPoint = 2,
GetArcPoint = 3,
Finish = 0
}
export class PlineCommand {
private state: CommandState = CommandState.GetFirstPoint;
private points: Point2D[] = [];
private bulges: number[] = [];
private isArcMode: boolean = false;
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
try {
while (this.state !== CommandState.Finish) {
switch (this.state) {
case CommandState.GetFirstPoint:
await this.stateGetFirstPoint();
break;
case CommandState.GetNextPoint:
await this.stateGetNextPoint();
break;
case CommandState.GetArcPoint:
await this.stateGetArcPoint();
break;
}
}
this.createPolyline();
} finally {
Engine.undoManager.end_undoMark();
Engine.clearPreview();
}
}
private async stateGetFirstPoint(): Promise<void> {
const options = new PointInputOptions("Specify start point:");
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
this.bulges.push(0);
this.state = CommandState.GetNextPoint;
} else {
this.state = CommandState.Finish;
}
}
private async stateGetNextPoint(): Promise<void> {
const lastPoint = this.points[this.points.length - 1];
const options = new PointInputOptions(
"Specify next point [Arc(A)/Close(C)/Undo(U)] <Done>:"
);
options.keywords = ["A", "C", "U"];
options.useBasePoint = true;
options.basePoint = lastPoint;
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
this.bulges.push(0);
} else if (result.stringResult === "A") {
this.isArcMode = true;
this.state = CommandState.GetArcPoint;
} else if (result.stringResult === "C") {
// Close and finish
if (this.points.length >= 3) {
this.state = CommandState.Finish;
} else {
writeMessage("<br/>At least 3 points are required to close.");
}
} else if (result.stringResult === "U") {
if (this.points.length > 1) {
this.points.pop();
this.bulges.pop();
} else {
this.state = CommandState.GetFirstPoint;
}
} else {
this.state = CommandState.Finish;
}
}
private async stateGetArcPoint(): Promise<void> {
const lastPoint = this.points[this.points.length - 1];
const options = new PointInputOptions(
"Specify arc endpoint [Line(L)/Undo(U)]:"
);
options.keywords = ["L", "U"];
options.useBasePoint = true;
options.basePoint = lastPoint;
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.points.push(result.value);
// Calculate bulge (simplified)
this.bulges.push(0.5);
} else if (result.stringResult === "L") {
this.isArcMode = false;
this.state = CommandState.GetNextPoint;
} else if (result.stringResult === "U") {
if (this.points.length > 1) {
this.points.pop();
this.bulges.pop();
}
this.state = CommandState.GetNextPoint;
} else {
this.state = CommandState.Finish;
}
}
private createPolyline(): void {
if (this.points.length < 2) return;
const pline = new PolylineEnt();
for (let i = 0; i < this.points.length; i++) {
pline.bulgePoints.add(new BulgePoint(this.points[i], this.bulges[i] || 0));
}
pline.setDefaults();
Engine.pcanvas.addEntity(pline);
Engine.undoManager.added_undoMark([pline]);
}
}Pattern 4: Loop Interaction Command
Uses while(true) to implement continuous user interaction until the user explicitly exits.
Characteristics
- Continuous loop until user exits
- Each loop iteration independently completes one operation
- Suitable for batch modification commands
Example: Trim Command
import { Engine, Point2D } from 'vjcad';
import {
getSelections,
getEntity,
SelectionInputOptions,
SelectionSetInputOptions,
InputStatusEnum,
writeMessage
} from 'vjcad';
export class TrimCommand {
private boundaries: any[] = [];
async main(): Promise<void> {
Engine.undoManager.start_undoMark();
try {
// Select trim boundaries
await this.selectBoundaries();
if (this.boundaries.length === 0) return;
// Trim loop
await this.trimLoop();
} finally {
Engine.undoManager.end_undoMark();
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();
}
}
private async selectBoundaries(): Promise<void> {
writeMessage("<br/>Select trim boundaries:");
const result = await getSelections(new SelectionInputOptions());
if (result.status === InputStatusEnum.OK && result.value.length > 0) {
this.boundaries = result.value;
writeMessage(`<br/>${this.boundaries.length} boundaries selected.`);
}
}
private async trimLoop(): Promise<void> {
// Highlight boundaries
Engine.pcanvas.highLightEntities(this.boundaries);
writeMessage("<br/>Select objects to trim (press Enter to finish):");
// Selection loop
while (true) {
const options = new SelectionSetInputOptions("Select object to trim:");
const result = await getEntity(options);
if (result.status === InputStatusEnum.OK && result.pickedEntity) {
// Execute trim
this.trimEntity(result.pickedEntity, result.pickedPoint);
Engine.pcanvas.regen();
} else if (result.status === InputStatusEnum.EnterOrSpace) {
break; // User pressed Enter to finish
} else if (result.status === InputStatusEnum.Cancel) {
break; // User pressed ESC to cancel
}
}
}
private trimEntity(entity: any, pickPoint: Point2D): void {
// Trim logic...
writeMessage(`<br/>Trimmed ${entity.type}`);
}
}Example: Offset Command
import { Engine, Point2D } from 'vjcad';
import {
getPoint,
getEntity,
PointInputOptions,
SelectionSetInputOptions,
InputStatusEnum,
ssSetFirst,
writeMessage
} from 'vjcad';
export class OffsetCommand {
private offsetDistance: number = 0;
async main(): Promise<void> {
ssSetFirst([]);
Engine.undoManager.start_undoMark();
try {
// Get offset distance
if (!await this.getDistance()) return;
// Offset loop
await this.offsetLoop();
} finally {
Engine.undoManager.end_undoMark();
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();
}
}
private async getDistance(): Promise<boolean> {
const options = new PointInputOptions(
`Enter offset distance <${Engine.OFFSETDIST}>:`
);
options.allowNumberResult = true;
const result = await getPoint(options);
if (result.status === InputStatusEnum.IsNumber) {
this.offsetDistance = result.numberValue;
Engine.OFFSETDIST = this.offsetDistance;
return true;
} else if (result.status === InputStatusEnum.EnterOrSpace) {
this.offsetDistance = Engine.OFFSETDIST;
return true;
}
return false;
}
private async offsetLoop(): Promise<void> {
while (true) {
// Select entity to offset
const selectOptions = new SelectionSetInputOptions("Select object to offset:");
const selectResult = await getEntity(selectOptions);
if (selectResult.status === InputStatusEnum.OK && selectResult.pickedEntity) {
// Highlight
Engine.pcanvas.drawControl.drawHighLightEntity(selectResult.pickedEntity);
Engine.pcanvas.redraw();
// Specify offset direction
const sideOptions = new PointInputOptions("Specify offset direction:");
sideOptions.useOsnap = false;
const sideResult = await getPoint(sideOptions);
Engine.pcanvas.clearHighLight();
if (sideResult.status === InputStatusEnum.OK) {
this.executeOffset(selectResult.pickedEntity, sideResult.value);
Engine.pcanvas.regen();
}
} else if (
selectResult.status === InputStatusEnum.EnterOrSpace ||
selectResult.status === InputStatusEnum.Cancel
) {
break;
}
}
}
private executeOffset(entity: any, sidePoint: Point2D): void {
// Offset logic...
writeMessage(`<br/>Offset ${entity.type}, distance ${this.offsetDistance}`);
}
}Pattern 5: Mode Switching Command
Supports switching between single execution and continuous execution modes.
Characteristics
- Can switch between single and continuous modes
- User can choose the operation mode
- Suitable for commands like copy, array, etc.
Example: Copy Command
import { Engine, Point2D } from 'vjcad';
import {
getSelections,
getPoint,
SelectionInputOptions,
PointInputOptions,
InputStatusEnum,
writeMessage
} from 'vjcad';
type CopyMode = 'Single' | 'Multi';
export class CopyCommand {
private selectedEntities: any[] = [];
private basePoint: Point2D = new Point2D();
private copyMode: CopyMode = 'Multi'; // Default to continuous copy
async main(): Promise<void> {
// Select entities
const selResult = await getSelections();
if (selResult.status !== InputStatusEnum.OK || selResult.value.length === 0) {
return;
}
this.selectedEntities = selResult.value;
// Highlight
Engine.pcanvas.highLightEntities(this.selectedEntities);
// Get base point
await this.getBasePoint();
// Execute copy
await this.copyLoop();
// Cleanup
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();
}
private async getBasePoint(): Promise<boolean> {
const message = this.copyMode === 'Single'
? "Specify base point [Multiple(M)]:"
: "Specify base point:";
const options = new PointInputOptions(message);
if (this.copyMode === 'Single') {
options.keywords = ["M"];
}
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.basePoint = result.value;
return true;
} else if (result.stringResult === "M") {
this.copyMode = 'Multi';
writeMessage("<br/>Continuous copy mode");
return await this.getBasePoint();
}
return false;
}
private async copyLoop(): Promise<void> {
while (true) {
const message = this.copyMode === 'Single'
? "Specify copy target [Multiple(M)]:"
: "Specify copy target <Done>:";
const options = new PointInputOptions(message);
options.useBasePoint = true;
options.basePoint = this.basePoint;
if (this.copyMode === 'Single') {
options.keywords = ["M"];
}
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.executeCopy(result.value);
// Exit after one copy in single mode
if (this.copyMode === 'Single') {
break;
}
} else if (result.stringResult === "M") {
this.copyMode = 'Multi';
writeMessage("<br/>Continuous copy mode");
} else {
break; // Done or cancel
}
}
}
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();
Engine.pcanvas.redraw();
}
}Choosing the Right Pattern
Next Steps
- Best Practices - Best practices for command development
- API Reference - Complete API reference