@huukhanhnguyen/model / commandRegistry/laneEdit
commandRegistry/laneEdit
EDITABLE_LANES
constEDITABLE_LANES: readonly ["materials","layers","patterns","styles","textStyles","annotationStyles","cameras","animations","components","textures","tables","views","sheets"] =LANE_NAMES
applyLaneEdit()
applyLaneEdit(
target,call):LaneEditCall|null
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).
Parameters
target
call
Returns
LaneEditCall | null
laneEntry()
laneEntry(
target,lane,key):LaneEntry|undefined
The entry itself, or undefined — the read half every section needs after an insert (laneInsert already returns it) or before an edit.
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
Returns
LaneEntry | undefined
laneEntryKey()
laneEntryKey(
lane,entry):string|undefined
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).
Parameters
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
entry
Returns
string | undefined
laneInsert()
laneInsert(
target,lane,entry,index?): {entry:LaneEntry;inverse:LaneEditCall;key:string; }
Insert an entry, minting/uniquing its identity. index defaults to the end.
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
entry
index?
number
Returns
{ entry: LaneEntry; inverse: LaneEditCall; key: string; }
entry
entry:
LaneEntry
inverse
inverse:
LaneEditCall
key
key:
string
laneKeys()
laneKeys(
target,lane):string[]
Every entry's identity in lane order.
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
Returns
string[]
laneMove()
laneMove(
target,lane,key,index): {inverse:LaneEditCall|null;moved:boolean; }
Move an entry to index (lane order is meaningful — CollectionNode.keys() dirties its readers on a reorder).
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
index
number
Returns
{ inverse: LaneEditCall | null; moved: boolean; }
inverse
inverse:
LaneEditCall|null
moved
moved:
boolean
laneReferencePaths()
laneReferencePaths(
target,lane,name):string[]
Paths of every arg statically referencing name — the delete-block report.
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
name
string
Returns
string[]
laneRemove()
laneRemove(
target,lane,key): {inverse:LaneEditCall|null;refs:string[];removed:boolean; }
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.
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
Returns
{ inverse: LaneEditCall | null; refs: string[]; removed: boolean; }
inverse
inverse:
LaneEditCall|null
refs
refs:
string[]
removed
removed:
boolean
laneReplace()
laneReplace(
target,lane,key,entry): {inverse:LaneEditCall; }
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 faceMaterial('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).
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
entry
Returns
{ inverse: LaneEditCall; }
inverse
inverse:
LaneEditCall
laneSetArg()
laneSetArg(
target,lane,key,argKey,input): {changed:boolean;inverse:LaneEditCall; }
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).
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
argKey
string
input
string | undefined
Returns
{ changed: boolean; inverse: LaneEditCall; }
changed
changed:
boolean
inverse
inverse:
LaneEditCall
laneSetField()
laneSetField(
target,lane,key,field,value): {changed:boolean;inverse:LaneEditCall|null; }
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).
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
field
string
value
unknown
Returns
{ changed: boolean; inverse: LaneEditCall | null; }
changed
changed:
boolean
inverse
inverse:
LaneEditCall|null
laneSetOps()
laneSetOps(
target,lane,key,operations,field?): {inverse:LaneEditCall; }
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.
Parameters
target
lane
"patterns" | "animations" | "materials" | "layers" | "styles" | "textStyles" | "annotationStyles" | "cameras" | "views" | "components" | "textures" | "tables" | "sheets"
key
string
operations
field?
LaneEditField = "operations"
Returns
{ inverse: LaneEditCall; }
inverse
inverse:
LaneEditCall
laneTouch()
laneTouch(
target):void
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.
Parameters
target
Returns
void
settingsSet()
settingsSet(
target,patch,replace?): {inverse:LaneEditCall; }
The one settings door. patch merges; a key set to undefined is removed. replace swaps the whole block (how the inverse restores it).
Parameters
target
patch
ModelSettings | undefined
replace?
boolean = false
Returns
{ inverse: LaneEditCall; }
inverse
inverse:
LaneEditCall
clearNodeArg
Re-exports clearNodeArg
nodeArgInput
Re-exports nodeArgInput
setNodeArg
Re-exports setNodeArg