Skip to content

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

  1. Multi-Level Undo: Support at least 100 levels of undo.
  2. Multi-Level Redo: Redo stack available until a new action invalidates it.
  3. Action Granularity: Each distinct user operation is one undoable action (not each low-level model change).
  4. Compound Actions: Group related changes (e.g., "move selection" moves multiple components as one undoable unit).
  5. 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
  6. Keyboard Shortcuts: Ctrl+Z (undo), Ctrl+Y / Ctrl+Shift+Z (redo).
  7. Menu Items: Edit → Undo (with action description), Edit → Redo (with action description).
  8. State Consistency: Undo/redo always leaves the circuit in a valid, consistent state.
  9. Memory Management: Old undo entries are discarded when the limit is reached (FIFO eviction).
  10. 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

  • MainViewModel owns the UndoStack.
  • 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

  1. Create IUndoableAction interface and UndoStack class in Core.
  2. Implement action classes for all supported operations.
  3. Implement CompoundAction for grouped changes.
  4. Integrate undo stack into MainViewModel.
  5. Refactor existing edit operations to create and execute actions through the stack.
  6. Add Ctrl+Z / Ctrl+Y keyboard bindings.
  7. Add Edit menu items with dynamic action descriptions.
  8. Implement dirty flag tracking for unsaved changes prompts.
  9. Add memory management (evict oldest entries beyond limit).
  10. 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.