Skip to content
Shapemetry

Model - Edit history

API reference for src/core/editHistory, src/schema/editHistory on @huukhanhnguyen/model, reachable from ..

See the Guide for the ModelJSON / evaluate pipeline.

API reference

Signatures are generated from the live package .d.ts - not hand-written.

createEditHistory

function

createEditHistory(target: (Model | ModelScope)): EditHistory

EDIT_HISTORY_MAX_STEPS

const

Max past steps retained per model.

MEASURED (2026-08-06, packages/parametric): JSON size of a typical inverse entry after JSON.stringify:

  • laneSetField / settingsSet / laneMove: ~80–200 bytes
  • laneInsert / laneRemove of a small sheet/view: ~150–600 bytes
  • laneSetOps with a short chain (≤5 ops): ~0.5–2 KB Stated memory budget: keep history JSON under ~2.5 MB per live model. Cap 500 steps × worst-case ~5 KB (large setOps body) ≈ 2.5 MB. Simple editing sessions stay well under that (500 × ~300 B ≈ 150 KB).
const EDIT_HISTORY_MAX_STEPS = 500

EditHistory

type

type EditHistory
// = {
    /** Completed undo steps (oldest first). */
    readonly past: readonly HistoryStep[];
    /** Steps undone and available to redo (oldest first among future). */
    readonly future: readonly HistoryStep[];
    /**
     * Door helpers call this after a successful mutation. No-op when
     * `inverse` is null (blocked remove, no-op move) or while undo/redo is
     * replaying through the door.
     */
    note(call: LaneEditCall, inverse: LaneEditCall | null | undefined): void;
    /** Push a single entry (clears redo). Used by tests and `apply`. */
    record(entry: HistoryEntry): void;
    /** Apply a call through the door and record the inverse when present. */
    apply(call: LaneEditCall): LaneEditCall | null;
    undo(): boolean;
    redo(): boolean;
    readonly canUndo: boolean;
    readonly canRedo: boolean;
    beginGroup(): void;
    endGroup(): void;
    withGroup<T>(fn: () => T): T;
    /**
     * Atomic multi-edit: run `fn` inside a group. On success the whole body is
     * one undo step (same as `withGroup`). On throw, every edit captured so
     * far is inverted in reverse order so the document is byte-identical to
     * the pre-transaction state, then the error is rethrown — no undo step is
     * left on the stack. Nested calls flatten into the outer transaction
     * (inner begin only deepens the group; only the outermost rolls back or
     * commits). Dirty / re-eval still fires per door edit during the body
     * (preserves the current group contract — batching evaluation is a
     * separate concern).
     */
    withTransaction<T>(fn: () => T): T;
    /** Drop past/future (model load / switch). Open groups are discarded. */
    clear(): void;
    /** Subscribe to stack changes (canUndo/canRedo UI). Returns unsubscribe. */
    onChange(listener: () => void): () => void;
}

HistoryEntry

type

One door edit and the inverse that restores the pre-edit document.

type HistoryEntry
// = {
    call: LaneEditCall;
    inverse: LaneEditCall;
}

HistoryStep

type

One undo step. Ungrouped edits have a single entry; a user gesture that emits several door calls is one step with multiple entries (undone in reverse order, redone in original order).

type HistoryStep
// = {
    entries: HistoryEntry[];
}
Last updated: 📖 1 min readEdit on GitHub