Repository navigation
Undo/Redo (@spatial-kit/history) #45
Description
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
Mapafter undoing aremove, 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/coregets 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)
-
createHistorywith 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
[-]Undo/Redo (@floormap/history)[/-][+]Undo/Redo (@spatial-kit/history)[/+]