Undo and Redo
Undo and Redo
WebCAD provides a complete undo / redo mechanism that supports rollback and re-execution for many kinds of operations. The undo system is a key feature for keeping user actions safe.
Overview
UndoManager
UndoManager is the core class of the undo / redo system.
import { Engine, UndoManager } from 'vjcad';
// Get undo manager
const undoMgr: UndoManager = Engine.undoManager;
// Undo
undoMgr.undo();
// Redo
undoMgr.redo();
// Check state
const canUndo = undoMgr.canUndo();
const canRedo = undoMgr.canRedo();Undo Record Types
Add Entities
import { Engine, LineEnt } from 'vjcad';
// Create and add entity (recommended: addEntities auto-records undo)
const line = new LineEnt([0, 0], [100, 100]);
line.setDefaults();
Engine.addEntities(line);
// Or manually record undo
const line2 = new LineEnt([0, 0], [50, 50]);
line2.setDefaults();
Engine.pcanvas.addEntity(line2);
Engine.undoManager.added_undoMark([line2]);Erase Entities
import { Engine, EntityBase } from 'vjcad';
// Erase entity
const entity: EntityBase = /* ... */;
Engine.currentDoc.currentSpace.erase([entity]);
// Record erase operation
Engine.undoManager.erased_undoMark([entity]);
// Or use Engine wrapper
Engine.erasedUndoMark([entity]);
// Simplest way: auto records undo
Engine.eraseEntities(entity);Modify Entities
import { Engine, EntityBase } from 'vjcad';
const entity: EntityBase = /* ... */;
// Method 1: use modEntity_undoMark
Engine.undoManager.modEntity_undoMark(entity); // Record state before modification
entity.color = 1;
entity.setModified();
// Method 2: use Engine.markModified
Engine.markModified(entity);
entity.layer = 'NewLayer';
entity.setModified();
// Batch modification
Engine.markModifiedBatch([entity1, entity2, entity3]);Move Entities
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const fromPoint = new Point2D(0, 0);
const toPoint = new Point2D(100, 100);
// Execute move
for (const entity of entities) {
entity.move(fromPoint, toPoint);
}
// Record move operation
Engine.undoManager.moved_undoMark(entities, fromPoint, toPoint);
// Or use Engine wrapper
Engine.movedUndoMark(entities, fromPoint, toPoint);Rotate Entities
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const basePoint = new Point2D(50, 50);
const angle = Math.PI / 4; // 45 degrees
// Execute rotation
for (const entity of entities) {
entity.rotate(basePoint, angle);
}
// Record rotation
Engine.undoManager.rotate_undoMark(entities, basePoint, angle);
// Or use Engine wrapper
Engine.rotateUndoMark(entities, basePoint, angle);Scale Entities
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const basePoint = new Point2D(50, 50);
const scale = 2.0;
// Execute scaling
for (const entity of entities) {
entity.scale(basePoint, scale);
}
// Record scaling
Engine.undoManager.scaled_undoMark(entities, basePoint, scale);
// Or use Engine wrapper
Engine.scaledUndoMark(entities, basePoint, scale);Mirror Entities
import { Engine, EntityBase, Point2D } from 'vjcad';
const entities: EntityBase[] = /* ... */;
const point1 = new Point2D(0, 0);
const point2 = new Point2D(100, 0); // Horizontal mirror line
// Execute mirror
for (const entity of entities) {
entity.mirror(point1, point2);
}
// Record mirror
Engine.undoManager.mirrored_undoMark(entities, point1, point2);
// Or use Engine wrapper
Engine.mirroredUndoMark(entities, point1, point2);Core Methods
| Method | Description |
|---|---|
canUndo() | Check whether undo is available |
canRedo() | Check whether redo is available |
undo() | Execute undo |
redo() | Execute redo |
start_undoMark(description?) | Start undo group with optional description (string | UndoDescriptor) |
end_undoMark() | End undo group |
getUndoHistory() | Get undo history list (returns UndoHistoryItem[]) |
getRedoHistory() | Get redo history list (returns UndoHistoryItem[]) |
getUndoCount() | Get number of undoable steps |
getRedoCount() | Get number of redoable steps |
undoSteps(countOrName, options?) | Undo by step count or name |
redoSteps(countOrName, options?) | Redo by step count or name |
rollbackLastGroup() | Silently roll back the last group |
Summary of Undo Record Types
| Method | Description | Undo Behavior |
|---|---|---|
added_undoMark(entities) | Record added entities | Delete those entities |
erased_undoMark(entities) | Record erased entities | Restore those entities |
modEntity_undoMark(entity) | Record entity modification | Restore original state |
moved_undoMark(entities, from, to) | Record move | Move back |
rotate_undoMark(entities, base, angle) | Record rotation | Reverse rotation |
scaled_undoMark(entities, base, scale) | Record scale | Reverse scaling |
mirrored_undoMark(entities, p1, p2) | Record mirror | Mirror again |
entLayer_undoMark(entities) | Record layer change | Restore original layer |
entColorIndex_undoMark(entities) | Record color change | Restore original color |
entLineType_undoMark(entities) | Record linetype change | Restore original linetype |
entLTScale_undoMark(entities) | Record linetype scale | Restore original scale |
entTransp_undoMark(entities) | Record transparency change | Restore original transparency |
clayer_undoMark(oldLayer) | Record current layer switch | Restore original current layer |
layerModified_undoMark(layer) | Record layer property change | Restore original properties |
Undo Groups
For complex operations with multiple steps, use undo groups to merge them into one undo unit.
startUndoRecord vs start_undoMark
Engine.startUndoRecord | undoMgr.start_undoMark | |
|---|---|---|
| Nesting | Safe — internal ref-count, only outermost takes effect | Unsafe — each call pushes a START marker |
| Use case | Commands (which may call each other, causing nesting) | UI code that will never nest (property panels, dialogs) |
| Recommendation | Use by default when unsure | Low-level control or performance-sensitive paths |
Basic Usage
import { Engine, LineEnt, CircleEnt } from 'vjcad';
const undoMgr = Engine.undoManager;
// Start undo group (with optional description)
undoMgr.start_undoMark('Create composite shape');
try {
const line = new LineEnt([0, 0], [100, 0]);
line.setDefaults();
line.color = 1;
Engine.addEntities(line);
const circle = new CircleEnt([50, 50], 30);
circle.setDefaults();
circle.color = 3;
Engine.addEntities(circle);
} finally {
undoMgr.end_undoMark();
}
// On undo, line and circle are deleted together.
// The command line shows "Undo completed. Create composite shape"Use Engine Wrapper Methods
import { Engine } from 'vjcad';
// Start undo record (supports nesting — recommended in commands)
Engine.startUndoRecord('Batch modify');
try {
Engine.addEntities([entity1, entity2]);
Engine.setEntityColor([entity1, entity2], 1);
} finally {
Engine.endUndoRecord();
}Nested Undo Groups
import { Engine } from 'vjcad';
// Nested calls are supported
Engine.startUndoRecord('Composite operation');
Engine.addEntities(entity1);
Engine.startUndoRecord(); // Nested (inner description is ignored)
Engine.addEntities(entity2);
Engine.endUndoRecord();
Engine.addEntities(entity3);
Engine.endUndoRecord();
// Only the outermost endUndoRecord actually creates the undo group
// entity1, entity2, entity3 are merged into one undo stepCheck Nested State
import { Engine } from 'vjcad';
if (Engine.isInUndoRecord()) {
console.log('Currently inside an undo group');
}Operation Descriptions
The undo system supports attaching description info (UndoDescriptor) to each undoable operation. Descriptions are shown in:
- Undo/redo button tooltips (e.g. "Undo: Modify Color")
- Undo/redo button dropdown history lists
- Command line messages (e.g. "Undo completed. Modify Color")
UndoDescriptor
import { UndoDescriptor } from 'vjcad';
interface UndoDescriptor {
/** i18n key for multi-language translation */
i18nKey?: string;
/** i18n parameters (reserved) */
params?: Record<string, string | number>;
/** Resolved text (displayed directly, takes priority over i18nKey) */
resolvedText?: string;
/** Category: edit, view, or system */
category: UndoCategory;
/** Source command name, e.g. 'LINE', 'MOVE' */
sourceCommand?: string;
}
type UndoCategory = 'edit' | 'view' | 'system';String shorthand: For simple edit descriptions, you can pass a plain string instead of the full object. start_undoMark('Draw Line') is equivalent to start_undoMark({ category: 'edit', resolvedText: 'Draw Line' }).
Description Resolution Priority
The label is resolved in this order:
resolvedText— used directly (highest priority)i18nKey— translated viat()then usedsourceCommand— command name as fallback- Generic text — "Undo" / "Redo"
Usage
// Method 1: plain text (string shorthand)
Engine.startUndoRecord('Batch modify color');
// Method 2: i18n key (recommended for multi-language)
Engine.startUndoRecord({ category: 'edit', i18nKey: 'undo.modify.color' });
// Method 3: specify source command
Engine.startUndoRecord({ category: 'edit', sourceCommand: 'MYCMD' });
// Method 4: no description — auto-derived in command context
Engine.startUndoRecord();Automatic Description Derivation
When start_undoMark() is called without a description, the system tries to derive one automatically:
- Check
Engine.editor.currentCommandName - Look up the command definition from
CommandRegistry - Use the command's
descriptionornameasresolvedText
This works when: commands are executed via Editor.runCommand(), which sets the command name automatically.
This does not work when: calling startUndoMark() from property panels, dialogs, or other non-command contexts — you must pass a description manually.
Operation Categories
| category | Purpose | UI Display |
|---|---|---|
edit | Edit operations (drawing, modifying properties, etc.) | History panel main list |
view | View operations (zoom, pan, UCS, etc.) | History panel collapsed group |
system | System operations (reserved) | — |
Query Undo History
import { Engine, UndoHistoryItem } from 'vjcad';
const undoMgr = Engine.undoManager;
// Get undo history list (most recent first)
const undoHistory: UndoHistoryItem[] = undoMgr.getUndoHistory();
// Get redo history list
const redoHistory: UndoHistoryItem[] = undoMgr.getRedoHistory();
// Get step counts
const undoCount: number = undoMgr.getUndoCount();
const redoCount: number = undoMgr.getRedoCount();
// Last undo/redo description
const lastUndo = undoMgr.lastUndoDescription;
const lastRedo = undoMgr.lastRedoDescription;UndoHistoryItem
interface UndoHistoryItem {
/** Operation description */
description?: UndoDescriptor;
/** Index in the history stack */
index: number;
/** Whether this is a grouped operation (wrapped by START/END) */
isGroup: boolean;
}Batch Undo / Redo
const undoMgr = Engine.undoManager;
// Undo 3 steps
undoMgr.undoSteps(3);
// Undo by name — undo to the most recent matching operation (inclusive), no-op if not found
undoMgr.undoSteps('Draw Line');
// Silent undo (no command line messages — useful for UI jump)
undoMgr.undoSteps(3, { silent: true });
// Redo 2 steps
undoMgr.redoSteps(2);
// Redo by name
undoMgr.redoSteps('Draw Line');
// Silently roll back the last group (for dialog cancel etc.)
undoMgr.rollbackLastGroup();Undo Stack Changed Event
The CadEvents.UndoStackChanged event fires whenever the undo/redo stack changes:
import { CadEventManager, CadEvents } from 'vjcad';
CadEventManager.getInstance().on(CadEvents.UndoStackChanged, (args) => {
console.log('Undo count:', args.undoCount);
console.log('Redo count:', args.redoCount);
console.log('Next undo:', args.nextUndoLabel);
console.log('Next redo:', args.nextRedoLabel);
});Event arguments:
interface UndoStackChangedEventArgs {
document: any;
undoCount: number;
redoCount: number;
nextUndoLabel?: string; // Translated label for the next undoable operation
nextRedoLabel?: string; // Translated label for the next redoable operation
}Undo Flowchart
Complete Command Example
import {
Engine,
LineEnt,
Point2D,
PointInputOptions,
InputStatusEnum
} from 'vjcad';
export class DrawLineCommand {
async main(): Promise<void> {
const undoMgr = Engine.undoManager;
const lines: LineEnt[] = [];
// Start undo group with description
undoMgr.start_undoMark({ category: 'edit', sourceCommand: 'LINE' });
try {
const opt1 = new PointInputOptions("Specify start point:");
const result1 = await Engine.getPoint(opt1);
if (result1.status !== InputStatusEnum.OK) return;
let startPoint = result1.value as Point2D;
while (true) {
const opt2 = new PointInputOptions("Specify next point or [Undo(U)]:");
opt2.basePoint = startPoint;
opt2.useBasePoint = true;
opt2.keywords = ["U"];
const result2 = await Engine.getPoint(opt2);
if (result2.status === InputStatusEnum.CANCEL) {
break;
}
if (result2.status === InputStatusEnum.KEYWORD) {
if (result2.stringResult === "U" && lines.length > 0) {
const lastLine = lines.pop()!;
Engine.currentDoc.currentSpace.erase([lastLine]);
undoMgr.erased_undoMark([lastLine]);
if (lines.length > 0) {
startPoint = lines[lines.length - 1].endPoint;
}
}
continue;
}
if (result2.status === InputStatusEnum.OK) {
const endPoint = result2.value as Point2D;
const line = new LineEnt(startPoint.clone(), endPoint.clone());
line.setDefaults();
Engine.pcanvas.addEntity(line);
undoMgr.added_undoMark([line]);
lines.push(line);
startPoint = endPoint;
}
}
} finally {
undoMgr.end_undoMark();
}
}
}Best Practices
1. Always Pair Start and End
// Good: use try-finally
undoMgr.start_undoMark('My operation');
try {
// operations...
} finally {
undoMgr.end_undoMark();
}
// Or use Engine wrapper
Engine.startUndoRecord('My operation');
try {
// operations...
} finally {
Engine.endUndoRecord();
}2. Put Batch Operations in One Undo Group
Engine.startUndoRecord('Batch add');
try {
for (const entity of entities) {
Engine.addEntities(entity);
}
} finally {
Engine.endUndoRecord();
}3. Record State Before Modification
// Good: record first, then modify
Engine.markModified(entity);
entity.color = 1;
entity.setModified();
// Bad: record after modification
entity.color = 1;
Engine.markModified(entity);4. Use Wrapper Methods to Simplify Code
// Recommended
Engine.setEntityColor(entities, 1);
Engine.setEntityLayer(entities, 'Layer1');
// Instead of manual operations
for (const entity of entities) {
Engine.markModified(entity);
entity.color = 1;
}5. Close Undo Groups Correctly in Conditional Logic
Engine.startUndoRecord('Conditional op');
try {
if (someCondition) {
Engine.addEntities(entity);
}
// Even if no operation runs, the group must still be closed properly
} finally {
Engine.endUndoRecord();
}6. Add Descriptions to Operations
// Recommended: use i18n key for multi-language support
Engine.startUndoRecord({ category: 'edit', i18nKey: 'undo.modify.color' });
// Simple cases: use plain text (string shorthand)
Engine.startUndoRecord('Modify Color');
// Non-command contexts (property panels etc.) must pass a description manually
startUndoMark({ category: 'edit', i18nKey: 'undo.modify.radius' });Next Steps
- Entity Base Class - understand the entity system
- Create Command - command development guide
- Engine System - detailed
EngineAPI