Forget everything. The canvas is left exactly as it is.
What has been done and can be undone, newest first — the order the menu renders.
Subscribe to every change to the history — a new entry, a span travelled, a clear.
What that span would do, played against a copy of the graph rather than summed from the entries — so a hide cancelled by a later unhide nets to zero, and a node a second run also vouches for is not counted as leaving.
Re-land the span from the line back through throughEntryId — the nearest
undone entry by default. Exactly as it was: no provider call, no re-gating, no
second trip through a hook.
OptionalthroughEntryId: stringwhat was actually re-applied, newest first.
What has been undone and can be redone, newest first. Emptied by any new action.
Undo the span from the newest entry through throughEntryId — the newest entry
alone by default. A sealed entry inside the span is passed over rather than
walled off: the entries around it are named element by element, so skipping one
cannot put the rest out of order.
The whole span lands as one dataBatchChanged, and one re-render.
OptionalthroughEntryId: stringwhat was actually reversed, newest first.
A bounded, session-scoped history of what the canvas holds and shows.
It is one timeline with a line drawn through it: everything above the line has happened, everything below has been undone and can be redone. Undo walks the line down through a contiguous span, redo walks it back up, and any new action strands whatever was below it. Nothing here reverses a single old entry on its own — that is
graph.removeBySource, which is a forward operation and says so.Reached as Graph.history.