Skip to content

Undo/Redo (@spatial-kit/history) #45

Open

Description

@lucasfelixs

Context

Editors need undo/redo. Today every change goes straight through core.add, core.update, core.updateMany and core.remove, and there is no history.

The plan is a separate, optional package, @spatial-kit/history, so the core stays small and apps that don't need history don't pay for it. This issue is design-first: agree on the approach in the comments before writing code.

Depends on: #44 (serialization) if a snapshot-based approach is chosen.


What changes (proposal)

Approach: inverse commands, recorded at the core API boundary. The history wraps a core and records each mutation together with its inverse:

Operation Inverse
add(entity) remove(entity.id)
remove(id) add(previousEntity), at the same position
update(id, patch) update(id, previousValues)
// @spatial-kit/history
const history = createHistory(core, { limit: 100 });

history.undo();
history.redo();
history.canUndo; // boolean
history.canRedo;

// group many changes into one undo step (e.g. a whole drag)
history.transaction(() => { /* ... */ });

history.on('change', ({ canUndo, canRedo }) => {});

Interactive gestures. One drag emits dozens of core.update calls. They must become one undo step. That means the SVG and React handlers need to mark where a gesture starts and ends, for example by emitting interaction:start / interaction:end events through core.emit. The history can then open and close a transaction around them. That handler change is probably a small, separate PR.

Alternative: snapshots. Store a serializeScene snapshot per step. It is simpler and always correct, but costs more memory on big scenes. Worth comparing in the discussion.


Open questions

  • Inverse commands or snapshots?
  • Does selection belong in the history? Proposal: no, but restore a sensible selection on undo.
  • How do we restore an entity's position in the Map after undoing a remove, so the z-order stays the same?
  • Should React get a useHistory(core) hook in the same package or in @spatial-kit/react?

Affected files

File Change
packages/history/ (new package) createHistory, tests, README, package.json mirroring core's setup
packages/svg/src/handlers.ts, packages/adapters/react/src/internal/attachHandlers.ts Emit gesture start/end events
.changeset, root README New package listed

Invariants to preserve

  • @spatial-kit/core gets no history-specific code and stays usable without the new package.
  • Undo followed by redo returns exactly the same scene.
  • No production dependencies; core is a peer dependency of the new package.

Implementation checklist

  • Agree on the design in this issue
  • Gesture start/end events in the handlers (separate PR)
  • New package scaffold (build, tests, exports, LICENSE, README)
  • createHistory with tests (add, update, remove, transactions, limit, redo cleared after a new change)
  • Wired into an example (Ctrl/⌘+Z, Shift+Ctrl/⌘+Z)
  • Changeset
  • Run pnpm typecheck && pnpm test && pnpm lint

Activity

  1. self-assigned this
    on Jun 7, 2026
  2. changed the title [-]Undo/Redo (@floormap/history)[/-] [+]Undo/Redo (@spatial-kit/history)[/+] on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions