Appearance
Spec: Undo/Redo Stack
Overview
Implement a comprehensive multi-level undo/redo system that tracks all user actions in the circuit editor and allows reverting or reapplying changes with standard keyboard shortcuts.
Motivation
Any design tool requires robust undo/redo. Circuit design involves frequent experimentation — placing components, moving them, drawing wires, deleting elements. Without undo, a single misclick can require significant manual repair. This is a fundamental usability requirement.
Requirements
Functional Requirements
- Multi-Level Undo: Support at least 100 levels of undo.
- Multi-Level Redo: Redo stack available until a new action invalidates it.
- Action Granularity: Each distinct user operation is one undoable action (not each low-level model change).
- Compound Actions: Group related changes (e.g., "move selection" moves multiple components as one undoable unit).
- Supported Actions:
- Add/remove component
- Add/remove wire
- Move component(s)
- Rotate/flip component
- Change component properties
- Add/remove annotations
- Group/ungroup components
- Wire vertex manipulation
- Cut/Copy/Paste
- Keyboard Shortcuts: Ctrl+Z (undo), Ctrl+Y / Ctrl+Shift+Z (redo).
- Menu Items: Edit → Undo (with action description), Edit → Redo (with action description).
- State Consistency: Undo/redo always leaves the circuit in a valid, consistent state.
- Memory Management: Old undo entries are discarded when the limit is reached (FIFO eviction).
- Dirty Flag: Track whether the circuit has unsaved changes based on undo position.
Non-Functional Requirements
- Undo/redo operations execute instantly (< 16ms for UI responsiveness).
- Memory footprint per action should be minimal (store deltas, not full snapshots).
- No impact on simulation performance.
Design
Architecture: Command Pattern
csharp
public interface IUndoableAction
{
string Description { get; }
void Execute();
void Undo();
}
public class UndoStack
{
private readonly Stack<IUndoableAction> _undoStack;
private readonly Stack<IUndoableAction> _redoStack;
private int _savePoint;
public bool CanUndo => _undoStack.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
public bool IsDirty => /* position != savePoint */;
public void Execute(IUndoableAction action);
public void Undo();
public void Redo();
public void MarkSaved();
}Action Examples
csharp
public class AddComponentAction : IUndoableAction
{
public string Description => $"Add {_component.Name}";
public void Execute() => _circuit.AddComponent(_component);
public void Undo() => _circuit.RemoveComponent(_component);
}
public class MoveComponentsAction : IUndoableAction
{
// Stores original positions and new positions
public string Description => $"Move {_components.Count} components";
public void Execute() => ApplyPositions(_newPositions);
public void Undo() => ApplyPositions(_originalPositions);
}
public class CompoundAction : IUndoableAction
{
private readonly List<IUndoableAction> _actions;
public string Description { get; }
public void Execute() => _actions.ForEach(a => a.Execute());
public void Undo() => _actions.AsEnumerable().Reverse().ToList().ForEach(a => a.Undo());
}Integration Points
MainViewModelowns theUndoStack.- All model-mutating operations go through the undo stack.
- Simulation state is NOT undoable (only design changes).
- File save/load resets undo stack (or optionally preserves it).
Implementation Tasks
- Create
IUndoableActioninterface andUndoStackclass in Core. - Implement action classes for all supported operations.
- Implement
CompoundActionfor grouped changes. - Integrate undo stack into
MainViewModel. - Refactor existing edit operations to create and execute actions through the stack.
- Add Ctrl+Z / Ctrl+Y keyboard bindings.
- Add Edit menu items with dynamic action descriptions.
- Implement dirty flag tracking for unsaved changes prompts.
- Add memory management (evict oldest entries beyond limit).
- Handle edge cases (undo during simulation, undo after file operations).
Risks & Open Questions
- Should undo history persist across save/load cycles?
- How to handle undo of property changes that affect simulation state?
- Should there be a visual undo history panel (like Photoshop)?
- Interaction with macros: can you undo changes inside a macro from the parent circuit?
Priority
High — Essential UX feature for any design tool.