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, orEMPTY_DOC, no selection and no history.preview(base, next, options?)settlesnextagainstbase.docfor drawing a gesture in progress. It adds no history. To cancel, drawbaseagain.commit(base, next, options?)settlesnext, diffs it againstbase.docand pushes one entry with the selection before and after. It clears the redo stack. An empty patch pushes nothing and returnsbase.doc, with the new selection.- Selection.
options.selectionreplaces 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 restoresselectionBefore. It does not settle again, so the exact old records come back.redo(editor)applies the next entry’s patch and restoresselectionAfter.- With nothing to replay, both return the same editor.
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 withparseShapeDocument, settles every record and returns a fresh editor: no history and no selection. Loading cannot be undone. Invalid input returns theParseError.- Saving needs no function of its own.
JSON.stringify(toShapeDocument(editor.doc))is the file text, andload(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.