Skip to content

Command/Undo Pattern for Interactive Sims

Most PhET-style simulations don't implement undo — Reset All covers the overwhelming majority of "let the user get back to a known state" needs, and continuous interactions (dragging, a slider) are their own undo in practice: dragging something back is usually as easy as the original drag. This page is for the minority of cases where a simulation genuinely needs step-by-step undo of discrete user actions (placing an object, applying an operation to build up a construction) — a structure to reach for if you need it, not something to build preemptively into every sim.

Why undo needs more structure than reset

reset() is the degenerate, simplest case of restoring state: everything goes back to one fixed, known configuration (see Reset All and State Persistence and Save/Restore Patterns). Undo is harder because it needs to restore to any of several previous states, one step at a time, in response to discrete user actions rather than a single reset button — which means something has to record, for every undoable action, both what changed and how to reverse it.

The command object shape

Structure each undoable user action as a small object (or plain function pair) with a do/execute step and a matching undo step, and push it onto a stack as the user performs actions:

ts
import type { Property } from 'scenerystack/axon';

interface Command {
  execute(): void;
  undo(): void;
}

class SetValueCommand<T> implements Command {
  private readonly previousValue: T;

  public constructor(
    private readonly property: Property<T>,
    private readonly newValue: T
  ) {
    this.previousValue = property.value;
  }

  public execute(): void {
    this.property.value = this.newValue;
  }

  public undo(): void {
    this.property.value = this.previousValue;
  }
}
ts
class CommandStack {
  private readonly undoStack: Command[] = [];
  private readonly redoStack: Command[] = [];

  public do( command: Command ): void {
    command.execute();
    this.undoStack.push( command );
    this.redoStack.length = 0; // a fresh action invalidates any previously-undone redo history
  }

  public undo(): void {
    const command = this.undoStack.pop();
    if ( command ) {
      command.undo();
      this.redoStack.push( command );
    }
  }

  public redo(): void {
    const command = this.redoStack.pop();
    if ( command ) {
      command.execute();
      this.undoStack.push( command );
    }
  }
}

Every user action that should be undoable goes through commandStack.do( new SomeCommand( ... ) ) instead of mutating model Properties directly — the same discipline as never bypassing model.reset() from Reset All, applied per-action instead of only at reset time.

Why this depends on Property-based state

This structure only works cleanly because of the same precondition State Persistence and Save/Restore Patterns already establishes: model state lives in enumerable Property values, not scattered plain fields or closures. A SetValueCommand capturing "the previous value of this Property" and restoring it later is exactly the "capture now, restore later" operation that page describes — undo is just that same mechanism triggered per discrete action instead of by one save/restore button. A model with state hidden outside Propertys has nothing for a command object to capture in the first place.

What belongs in a command, and what doesn't

Belongs in a commandDoesn't belong in a command
A discrete, user-initiated change to model state (placing an object, setting a value, applying an operation)Continuous interaction in progress (an active drag) — undo the result of the drag as one command on release, not every intermediate frame
Enough captured state to fully reverse the change (previousValue above)View-only state with no model consequence — nothing to undo if nothing in the model changed
Composite actions, as a single command wrapping several Property changes that should undo together atomicallyAnything already covered by Reset All — don't build a parallel "undo everything" when a reset button already exists for that

Most sims should not build this speculatively

Undo/redo is real architectural surface area — every discrete action needs a command object, the stack needs disposal discipline of its own (see Dispose and Memory Management if commands hold references to disposable Properties or Nodes), and testing has to cover do/undo/redo sequences, not just the forward path. Build this structure when a simulation's design genuinely calls for step-by-step undo — a construction/building tool is the common case — not by default. For everything else, Reset All is sufficient and dramatically simpler.