Undo/Redo Mechanism
Undo/Redo Mechanism
WebCAD provides a comprehensive undo/redo mechanism to ensure users can revert and restore operations. This chapter explains in detail how to properly implement undo support in commands.
Core Concepts
Undo Mark Group
An Undo Mark Group combines multiple operations into a single undoable unit. All operations within a command should be in the same mark group, so that a single undo action reverts the entire command.
// Start undo mark group (optional description shows the operation name on undo)
Engine.undoManager.start_undoMark('Draw Line');
try {
// Command operations...
// All operations within this scope will be undone as a whole
} finally {
// End undo mark group
Engine.undoManager.end_undoMark();
}
// Or use the nesting-safe wrapper (recommended)
Engine.startUndoRecord('Draw Line');
try {
// Command operations...
} finally {
Engine.endUndoRecord();
}Tip: The
descriptionparameter ofstart_undoMarkis optional. In command context (executed viaEditor.runCommand), the system auto-derives description from the command name. See Operation Descriptions for details.
Undo Records
Within a mark group, each specific operation needs to be recorded so that it can be correctly restored when undoing.
Basic Usage
Adding Entities
When a command creates new entities, record the add operation:
import { Engine, LineEnt, Point2D } from 'vjcad';
// Create entity
const line = new LineEnt(
new Point2D(0, 0),
new Point2D(100, 100)
);
line.setDefaults();
// Add to canvas
Engine.pcanvas.addEntity(line);
// Record undo information
Engine.undoManager.added_undoMark([line]);Deleting Entities
When a command deletes entities, record the delete operation:
// Get entities to delete
const entities = [...selectedEntities];
// Execute deletion
Engine.currentDoc.currentSpace.erase(entities);
// Record undo information
Engine.undoManager.erased_undoMark(entities);Moving Entities
When a command moves entities, record the move operation:
const fromPoint = new Point2D(0, 0);
const toPoint = new Point2D(100, 50);
// Execute move
for (const entity of entities) {
entity.move(fromPoint, toPoint);
}
// Record undo information
Engine.undoManager.moved_undoMark(entities, fromPoint, toPoint);Rotating Entities
When a command rotates entities, record the rotate operation:
const center = new Point2D(50, 50);
const angle = Math.PI / 4; // 45 degrees
// Execute rotation
for (const entity of entities) {
entity.rotate(center, angle);
}
// Record undo information
Engine.undoManager.rotate_undoMark(entities, center, angle);Mirroring Entities
When a command mirrors entities, record the mirror operation:
const mirrorPoint1 = new Point2D(0, 0);
const mirrorPoint2 = new Point2D(0, 100); // Vertical mirror line
// Execute mirror
for (const entity of entities) {
entity.mirror(mirrorPoint1, mirrorPoint2);
}
// Record undo information
Engine.undoManager.mirrored_undoMark(entities, mirrorPoint1, mirrorPoint2);Modifying Entity Properties
When a command modifies entity properties, record the state before modification:
// Record original state before modification
Engine.undoManager.modEntity_undoMark(entity);
// Then modify the entity
entity.color = 1; // Red
entity.layer = "NewLayer";Undo API Reference
UndoManager Core Methods
| Method | Parameters | Description |
|---|---|---|
canUndo() | - | Check if undo is available |
canRedo() | - | Check if redo is available |
undo() | - | Execute undo operation |
redo() | - | Execute redo operation |
start_undoMark(description?) | string | UndoDescriptor (optional) | Start undo mark group with optional description |
end_undoMark() | - | End undo mark group |
getUndoHistory() | - | Get undo history list |
getRedoHistory() | - | Get redo history list |
undoSteps(countOrName, options?) | number | string, { silent? } | Undo by step count or name |
redoSteps(countOrName, options?) | number | string, { silent? } | Redo by step count or name |
UndoManager Record Methods
| Method | Parameters | Description |
|---|---|---|
added_undoMark(entities) | EntityBase[] | Record entity addition |
erased_undoMark(entities) | EntityBase[] | Record entity deletion |
moved_undoMark(entities, from, to) | EntityBase[], Point2D, Point2D | Record move |
rotate_undoMark(entities, center, angle) | EntityBase[], Point2D, number | Record rotation |
mirrored_undoMark(entities, pt1, pt2) | EntityBase[], Point2D, Point2D | Record mirror |
modEntity_undoMark(entity) | EntityBase | Record entity modification (call before modifying) |
modLine_undoMark(line) | LineEnt | Record line modification |
modArc_undoMark(arc) | ArcEnt | Record arc modification |
modRay_undoMark(ray) | RayEnt | Record ray modification |
Complete Examples
Example 1: Draw Line Command
import { Engine, LineEnt, Point2D } from 'vjcad';
import {
getPoint,
PointInputOptions,
InputStatusEnum,
ssSetFirst
} from 'vjcad';
export class LineCommand {
private points: Point2D[] = [];
async main(): Promise<void> {
ssSetFirst([]);
// Start undo mark group
Engine.undoManager.start_undoMark();
try {
// Get first point
const p1Result = await getPoint(new PointInputOptions("Specify first point:"));
if (p1Result.status !== InputStatusEnum.OK) return;
this.points.push(p1Result.value);
// Continuously get more points
while (true) {
const options = new PointInputOptions("Specify next point [Undo(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];
// Create line
const line = new LineEnt(lastPoint, newPoint);
line.setDefaults();
Engine.pcanvas.addEntity(line);
// Record undo
Engine.undoManager.added_undoMark([line]);
this.points.push(newPoint);
} else if (result.stringResult === "U") {
// Undo last line
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 {
// Ensure undo mark group is ended
Engine.undoManager.end_undoMark();
}
}
}Example 2: Copy Command
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> {
// Select entities
const selResult = await getSelections(new SelectionInputOptions());
if (selResult.status !== InputStatusEnum.OK || selResult.value.length === 0) {
return;
}
this.selectedEntities = selResult.value;
// Get base point
const baseResult = await getPoint(new PointInputOptions("Specify base point:"));
if (baseResult.status !== InputStatusEnum.OK) return;
this.basePoint = baseResult.value;
// Continuous copy
while (true) {
const targetOptions = new PointInputOptions("Specify copy target point <Done>:");
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 {
// Note: Each copy is an independent undo unit
Engine.undoManager.start_undoMark();
const copiedEntities: any[] = [];
for (const entity of this.selectedEntities) {
// Clone entity
const cloned = entity.clone();
// Move to target position
cloned.move(this.basePoint, targetPoint);
// Add to canvas
Engine.pcanvas.addEntity(cloned);
copiedEntities.push(cloned);
}
// Record undo
Engine.undoManager.added_undoMark(copiedEntities);
Engine.undoManager.end_undoMark();
}
}Example 3: Modify Entity Properties
import { Engine } from 'vjcad';
import {
getSelections,
InputStatusEnum,
writeMessage
} from 'vjcad';
export class ChangeColorCommand {
async main(): Promise<void> {
// Select entities
const result = await getSelections();
if (result.status !== InputStatusEnum.OK || result.value.length === 0) {
return;
}
const entities = result.value;
const newColor = 1; // Red
Engine.undoManager.start_undoMark();
try {
for (const entity of entities) {
// Record original state before modification
Engine.undoManager.modEntity_undoMark(entity);
// Modify color
entity.color = newColor;
}
Engine.pcanvas.regen();
writeMessage(`<br/>Modified color of ${entities.length} entities.`);
} finally {
Engine.undoManager.end_undoMark();
}
}
}Example 4: Trim Line
import { Engine, LineEnt, Point2D } from 'vjcad';
function trimLineAtPoint(line: LineEnt, trimPoint: Point2D): void {
Engine.undoManager.start_undoMark();
try {
// Record state before modification
Engine.undoManager.modLine_undoMark(line);
// Modify line endpoint
line.startPoint = trimPoint;
Engine.pcanvas.regen();
} finally {
Engine.undoManager.end_undoMark();
}
}Example 5: Complex Transform (Move + Rotate + Mirror)
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 {
// Use helper functions
startUndoMark();
try {
const verticalMirrorLine = new Point2D(basePoint.x, basePoint.y + 1);
const horizontalMirrorLine = new Point2D(basePoint.x + 1, basePoint.y);
// Execute transforms
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);
}
// Record undo in order (consistent with execution order)
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();
}Operation Descriptions
start_undoMark accepts an optional string | UndoDescriptor so the undo/redo action shows a descriptive label in the command line and UI (e.g. "Undo completed. Draw Line"). A plain string is shorthand for { category: 'edit', resolvedText: str }.
// Method 1: plain text (string shorthand)
Engine.undoManager.start_undoMark('Draw Line');
// Method 2: i18n key (recommended for multi-language)
Engine.undoManager.start_undoMark({ category: 'edit', i18nKey: 'undo.op.add' });
// Method 3: omit in command context — auto-derived from command name
Engine.undoManager.start_undoMark();Query the undo history list:
const history = Engine.undoManager.getUndoHistory();
history.forEach(item => {
console.log(item.description?.resolvedText);
});For full details, see Undo / Redo - Operation Descriptions.
Best Practices
1. Use try-finally to Ensure Mark Completion
Engine.undoManager.start_undoMark();
try {
// Operations that may throw exceptions
await riskyOperation();
} finally {
// End mark regardless of success or failure
Engine.undoManager.end_undoMark();
}2. Use a Single Mark Group for Batch Operations
// Good practice: All operations in one mark group
Engine.undoManager.start_undoMark();
for (const entity of entities) {
entity.move(fromPoint, toPoint);
}
Engine.undoManager.moved_undoMark(entities, fromPoint, toPoint);
Engine.undoManager.end_undoMark();
// Bad practice: One mark group per entity
for (const entity of entities) {
Engine.undoManager.start_undoMark(); // Don't do this!
entity.move(fromPoint, toPoint);
Engine.undoManager.moved_undoMark([entity], fromPoint, toPoint);
Engine.undoManager.end_undoMark();
}3. Record All Changes
// Good practice: 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 practice: Missing delete operation record
Engine.undoManager.start_undoMark();
Engine.pcanvas.addEntity(newEntity);
Engine.undoManager.added_undoMark([newEntity]);
Engine.currentSpace.erase([oldEntity]);
// Forgot to record! Undo will have issues
Engine.undoManager.end_undoMark();4. Record Before Modifying Properties
// Correct: Record before modification
Engine.undoManager.modEntity_undoMark(entity);
entity.color = newColor;
// Wrong: Record after modification (cannot restore original value)
entity.color = newColor;
Engine.undoManager.modEntity_undoMark(entity); // Too late!5. Mark Groups Must Be Paired
// Correct: Paired usage
Engine.undoManager.start_undoMark();
// ...
Engine.undoManager.end_undoMark();
// Wrong: Started but not ended
Engine.undoManager.start_undoMark();
// ...
// Forgot end_undoMark()! Subsequent commands will have issuesFAQ
Q: Why isn't undo working?
Check the following:
- Whether
start_undoMark()andend_undoMark()were called - Whether the corresponding record methods (e.g.,
added_undoMark) were called correctly - Whether the parameters to record methods are correct
Q: How to implement in-command undo (like the U option in the LINE command)?
In-command undo requires manual management:
if (result.stringResult === "U") {
// Remove the last added entity
Engine.currentDoc.currentSpace.items.pop();
// Remove the corresponding undo record
Engine.undoManager.items.pop();
// Refresh display
Engine.pcanvas.regen();
}Q: How to organize undo groups for continuous operations?
It depends on user expectations:
- If the user expects a single undo to revert the entire command, use a single mark group
- If the user expects step-by-step undo, use independent mark groups for each step
Next Steps
- Preview Drawing - Learn entity preview
- Command Design Patterns - Learn different command design patterns
- Best Practices - More command development best practices