Skip to content

Editor and history

type Selection = ReadonlySet<ShapeId>;
type Entry = {
readonly patch: Patch;
readonly selectionBefore: Selection;
readonly selectionAfter: Selection;
};
type History = { readonly past: readonly Entry[]; readonly future: readonly Entry[] };
type Editor = { readonly doc: Doc; readonly selection: Selection; readonly history: History };
type CommitOptions = { readonly selection?: Selection };

Every function returns a new Editor. The document of an editor returned by createEditor, commit, undo, redo or load has nothing touched, so a gesture that starts from it diffs only its own changes.

Editing

  • createEditor(doc?) starts with the given document, or EMPTY_DOC, no selection and no history.
  • preview(base, next, options?) settles next against base.doc for drawing a gesture in progress. It adds no history. To cancel, draw base again.
  • commit(base, next, options?) settles next, diffs it against base.doc and pushes one entry with the selection before and after. It clears the redo stack. An empty patch pushes nothing and returns base.doc, with the new selection.
  • Selection. options.selection replaces the selection. Ids that are not in the settled document are dropped, so the selection only holds existing shapes.

Undo and redo

  • undo(editor) applies the last entry’s inverted patch and restores selectionBefore. It does not settle again, so the exact old records come back.
  • redo(editor) applies the next entry’s patch and restores selectionAfter.
  • With nothing to replay, both return the same editor.
docs/examples/undo-bound-arrow.ts
import { commit, createEditor, fromShapeDocument, undo, updateShapes } from "@draw/editor";
import type { Editor } from "@draw/editor";
import {
DEFAULT_STYLE,
bindingId,
indexAbove,
localPoint,
normalizedPoint,
radians,
shapeId,
} from "@draw/shapes";
import type { Shape } from "@draw/shapes";
const box: Shape = {
id: shapeId("box"),
frame: { x: 300, y: 0, width: 100, height: 100, rotation: radians(0) },
drawable: { kind: "rectangle", cornerRadius: 0 },
index: indexAbove([]),
style: DEFAULT_STYLE,
groupIds: [],
};
const arrow: Shape = {
id: shapeId("arrow"),
frame: { x: 0, y: 50, width: 300, height: 0, rotation: radians(0) },
drawable: {
kind: "arrow",
start: localPoint(0, 0),
end: localPoint(300, 0),
bend: 0,
startHead: "none",
endHead: "arrow",
},
index: indexAbove([box]),
style: DEFAULT_STYLE,
groupIds: [],
};
const start: Editor = createEditor(
fromShapeDocument({
shapes: [box, arrow],
bindings: [
{
id: bindingId("b1"),
arrowId: arrow.id,
terminal: "end",
targetId: box.id,
anchor: normalizedPoint(0, 0.5),
},
],
}),
);
export const moved: Editor = commit(
start,
updateShapes(start.doc, [box.id], (shape) => ({
...shape,
frame: { ...shape.frame, x: shape.frame.x + 100 },
})),
);
export const undone: Editor = undo(moved);
export const restoredArrow: boolean = undone.doc.shapes.get(arrow.id) === arrow;

Moving the box re-resolves the bound arrow in the same commit, so the patch holds both shapes and one undo restores both original objects.

Selection and loading

  • select(editor, ids) changes only the selection, keeping ids in the document. It adds no history entry. A selection is recorded with each edit, but is not undone on its own.
  • load(input) parses untrusted input with parseShapeDocument, settles every record and returns a fresh editor: no history and no selection. Loading cannot be undone. Invalid input returns the ParseError.
  • Saving needs no function of its own. JSON.stringify(toShapeDocument(editor.doc)) is the file text, and load(JSON.parse(text)) restores it. Shapes, bindings, indices and seeds survive exactly, so a reloaded document draws the same pixels. Reading and writing the file is the host’s job, because this package does no I/O.