Undo and redo
Every operation on the partition is recorded together with its exact inverse. Undo applies the inverses, redo the operations again — no snapshots, no approximations. After an undo, the partition is the same as before the edit, IDs included.
One step per gesture#
One transaction is one undo step, however many operations it contains:
- drawing a polygon that splits three faces — one step;
- removing or merging a whole selection — one step;
- moving a face to another layer, which changes two partitions — one step;
- labeling, and styling a label — one step each.
A transaction that changes nothing (a click without movement, the same outline drawn again)
creates no step; the editor reports unchanged.
Not in the history: selection, active tool, and the layer list itself — adding, renaming,
reordering, hiding, locking or removing a layer. Their content changes are: clearing a layer
(editor.ebeneLeeren) is one undoable step. A layer can only be removed when it is empty, so
nothing is lost without undo.
The history is shared by all layers of an editor: undo takes back the last step, whichever layer it changed.
Calling undo and redo#
editor.undo(); // false if there was nothing to undo, or the undo was rejected
editor.redo(); // false if there was nothing to redo, or the redo was rejected
editor.history.canUndo();
editor.history.canRedo();
A successful undo or redo emits change with the label of the step it moved — undoing a
polygon emits change with 'draw-polygon'. Only a rejected undo or redo carries the label
'undo' or 'redo' (see Validity and rejections).
A tool in the middle of a gesture that refers to existing elements — a vertex being dragged, the
first click of split or union, a transform drag — notices the changed partition and abandons the
gesture instead of working on elements that may be gone.
A new edit after an undo discards the steps that could have been redone.
Keys and buttons#
The editor binds no keys for undo and redo — which keys mean what is your application's choice. This example binds Ctrl+Z / Cmd+Z and Shift+Ctrl+Z / Shift+Cmd+Z, leaves text fields alone, and keeps two buttons enabled only while there is something to undo or redo:
import type { Editor } from '@vectorstream/editor';
/**
* Ctrl+Z / Cmd+Z undoes, with Shift it redoes. The editor ships no system shortcuts of its own:
* your application decides which keys mean what. Typing in a text field keeps its own undo.
*/
export function bindUndoKeys(editor: Editor, target: Document = document): () => void {
const onKey = (e: KeyboardEvent): void => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'z') return;
e.preventDefault();
if (e.shiftKey) editor.redo();
else editor.undo();
};
target.addEventListener('keydown', onKey);
return () => target.removeEventListener('keydown', onKey);
}
/** Undo/redo buttons that are disabled while there is nothing to undo or redo. */
export function bindUndoButtons(editor: Editor, undo: HTMLButtonElement, redo: HTMLButtonElement): () => void {
const update = (): void => {
undo.disabled = !editor.history.canUndo();
redo.disabled = !editor.history.canRedo();
};
const onUndo = (): void => void editor.undo();
const onRedo = (): void => void editor.redo();
undo.addEventListener('click', onUndo);
redo.addEventListener('click', onRedo);
// `change` follows every new entry, every undo and every redo; `rejected` may follow an undo.
const off = [editor.on('change', update), editor.on('rejected', update)];
update();
return () => {
off.forEach((unsubscribe) => unsubscribe());
undo.removeEventListener('click', onUndo);
redo.removeEventListener('click', onRedo);
};
}
API: Editor.undo, History.