Commands & History
Every undoable mutation runs through a Command. History executes commands and holds
the undo/redo stacks.
import { History, SetPropsCommand, AddNodeCommand, RemoveNodeCommand, CompositeCommand,} from '@veyrajs/core'import type { Command, NodeProps, HistoryListener } from '@veyrajs/core'Command
Section titled “Command”A reversible operation.
interface Command { do(): void undo(): void label?: string // for UI (e.g. "Undo move")}
interface NodeProps { // the transform & visual props a SetPropsCommand can change (all optional)}Built-in commands
Section titled “Built-in commands”Each implements Command.
// set props; undo restores `before`class SetPropsCommand implements Command { constructor(node: Node, before: NodeProps, after: NodeProps, label?: string)}
// add `node` to `parent`; undo removes itclass AddNodeCommand implements Command { constructor(parent: Container, node: Node, label?: string)}
// remove `node`; undo re-adds it to the previous parentclass RemoveNodeCommand implements Command { constructor(node: Node, label?: string)}
// group; runs children forward on do(), in reverse on undo()class CompositeCommand implements Command { constructor(commands: Command[], label?: string)}SetPropsCommandcaptures values, not deltas — robust to intervening edits and trivial to invert.RemoveNodeCommand’s undo re-adds at the top of the z-order, not the original index (an MVP limitation).- Commands hold node references; if a node is removed or replaced (e.g. by a scene reload), clear the history.
history.run(new AddNodeCommand(layer, rect))history.push(new SetPropsCommand(rect, { x: 0 }, { x: 120 }, 'move')) // already appliedHistory
Section titled “History”The undo/redo stack. run executes a command and records it; push records one that has already
been applied.
type HistoryListener = () => void
class History { run(command: Command): void // command.do() then push push(command: Command): void // record an already-applied command
undo(): void redo(): void clear(): void
get canUndo(): boolean get canRedo(): boolean
onChange(listener: HistoryListener): () => void // returns an unsubscribe fn}- Linear history. A fresh
run/pushafter some undos discards the redo branch. - Bounded. The oldest entry is dropped past the limit (default
200). onChangefor UI. Every mutation emits a change; bind buttondisabledstate tocanUndo/canRedo.- Clear after a scene reload — loading a document replaces every node instance, invalidating any recorded commands.
const history = new History()const controller = new SelectionController(stage, { history })history.onChange(() => { undoBtn.disabled = !history.canUndo })