Documentation

Pro handbook

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.

See it on a map.