Skip to content
Shapemetry

Model - Lane edit

API reference for src/commandRegistry/laneEdit 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.

applyLaneEdit

function

Replay one lane edit (undo/redo, and D2's command log). Returns the inverse of the applied edit, or null when the helper was a no-op (blocked remove, no-op move, refused rename).

applyLaneEdit(target: (Model | ModelScope), call: LaneEditCall): LaneEditCall | null

EDITABLE_LANES

const

Lane-entry edit API — the ONE door for editing a record/chain lane (materials, layers, styles, views, sheets, components, tables, textures, cameras, skeletons, animations).

The node-graph half of the mutation surface has been one door for a long time (setInput/setKey/addOperation/NodeCollection add-remove-move). The lane half had none: every panel and chat action reached into scope.<lane>.push/splice, assigned whole arrays, and then remembered — or did not remember — to fire the collection's "change" event by hand. This module closes that half. Every function here

  1. validates the lane, the entry key, and the field/arg it is asked to write,
  2. applies the edit,
  3. OWNS the notification (laneTouch — the same "change" event touch(owner) fires in packages/ui, which is what render sync, the lane nodes' hasChanged pass, and autosave all hang off), and
  4. returns a SERIALIZABLE inverse (LaneEditCall) that applyLaneEdit can execute — the undo unit S2's command log (D2) is built from.

Identity is per lane: most lanes key entries by key; materials/layers/ styles carry identity in a quoted literal name ARG instead (they are material/layer/style nodes — see materials.ts/layers.ts/styles.ts), so key in these signatures means "that lane's identity value".

NOT in scope here (deliberate): args of a step NESTED inside a chain entry's operations. Addressing one needs a node PATH (RigSection's joints are a tree, not a flat list), and those editors already funnel through their own commit door. They share this module's setNodeArg mechanics; the path-addressed command lands with D2.

const EDITABLE_LANES: readonly ["materials", "layers", "styles", "textStyles", "annotationStyles", "cameras", "skeletons", "animations", "components", "textures", "tables", "views", "sheets"]

laneEntry

function

The entry itself, or undefined — the read half every section needs after an insert (laneInsert already returns it) or before an edit.

laneEntry(target: (Model | ModelScope), lane: LaneName, key: string): LaneEntry | undefined

laneEntryKey

function

A lane entry's identity value — its key, or the literal its name arg evaluates to. Undefined when the lane keys by name and the name is a real expression (not statically addressable, same limit the validator accepts).

laneEntryKey(lane: LaneName, entry: LaneEntry): string | undefined

laneInsert

function

Insert an entry, minting/uniquing its identity. index defaults to the end.

laneInsert(target: (Model | ModelScope), lane: LaneName, entry: LaneEntry, index?: number): { entry: LaneEntry; key: string; inverse: LaneEditCall; }

laneKeys

function

Every entry's identity in lane order.

laneKeys(target: (Model | ModelScope), lane: LaneName): string[]

laneMove

function

Move an entry to index (lane order is meaningful — CollectionNode.keys() dirties its readers on a reorder).

laneMove(target: (Model | ModelScope), lane: LaneName, key: string, index: number): { moved: boolean; inverse: LaneEditCall | null; }

laneReferencePaths

function

Paths of every arg statically referencing name — the delete-block report.

laneReferencePaths(target: (Model | ModelScope), lane: LaneName, name: string): string[]

laneRemove

function

Remove an entry — REFUSED (with the referencing paths) while a by-name consumer still points at it, on every lane that has by-name consumers. Unlike the other doors this is TOLERANT of an unknown key (a no-op): the callers are ✕ glyphs holding a captured entry, and a double click must not throw out of a click handler.

laneRemove(target: (Model | ModelScope), lane: LaneName, key: string): { removed: boolean; refs: string[]; inverse: LaneEditCall | null; }

laneReplace

function

Replace a lane entry WHOLE, keeping its position and its identity.

The door an entry-level diff needs, and the one this module was missing: swap an entry outright rather than reaching inside it for a minimal edit (root CLAUDE.md, "The object is the unit of change").

Remove-then-insert cannot do this and must not be used for it. laneRemove REFUSES to delete an entry another node references by name — a material named "Oak" that some applyMaterial('Oak') still points at stays put — and laneInsert uniques the identity it mints, so the pair would silently leave the old entry in place beside a renamed copy. Editing in place sidesteps both: nothing is ever unreferenced, and the identity never moves.

The incoming entry's identity must match key; a rename is a different operation with different consequences (it rewrites every reference).

laneReplace(target: (Model | ModelScope), lane: LaneName, key: string, entry: LaneEntry): { inverse: LaneEditCall; }

laneSetArg

function

Write one arg on the lane ENTRY's own node (materials/layers/styles/ cameras are single nodes; a chain entry's own args, where it has them). input is an expression STRING — never evaluated here; undefined REMOVES the arg (the old clearMaterialArg/clearLayerArg/ clearStyleArg door, and what an inverse restores to when there was no arg).

laneSetArg(target: (Model | ModelScope), lane: LaneName, key: string, argKey: string, input: string | undefined): { changed: boolean; inverse: LaneEditCall; }

laneSetField

function

Write one header field (key/title/label/enabled/flat). A key rename routes through the lane's rename semantics: a by-name lane rewrites every reference doc-wide, and a renamed view carries settings.activeView with it. Returns changed=false on an empty or colliding key (the caller reverts the field, matching what every section already does).

laneSetField(target: (Model | ModelScope), lane: LaneName, key: string, field: string, value: unknown): { changed: boolean; inverse: LaneEditCall | null; }

laneSetOps

function

Replace a chain entry's whole body (the views/components/ sheets subtree editors' write-back). field selects which body — default operations; a components[] entry's opening is the other one.

laneSetOps(target: (Model | ModelScope), lane: LaneName, key: string, operations: (Operation | Container)[], field?: LaneEditField): { inverse: LaneEditCall; }

laneTouch

function

Fire the root collection's "change" — the SAME signal touch(owner) fires in packages/ui, which render sync, the lane nodes' hasChanged pass and the builder's reactive blocks all subscribe to. Owned here so a lane edit can no longer forget it.

laneTouch(target: (Model | ModelScope)): void

settingsSet

function

The one settings door. patch merges; a key set to undefined is removed. replace swaps the whole block (how the inverse restores it).

settingsSet(target: (Model | ModelScope), patch: ModelSettings | undefined, replace?: boolean): { inverse: LaneEditCall; }
Last updated: 📖 4 min readEdit on GitHub