Skip to content

toggleNode desyncs the tree from undo history #54

Description

@kmatzen

toggleNode (src/store/modelerStore.ts:260) mutates the document without recording it:

toggleNode: (id) => {
  const { tree } = get();
  if (!tree) return;
  const newTree = updateInTree(tree, id, (node) => ({ ...node, enabled: !node.enabled }));
  set({ tree: newTree });   // no history push
},

Every other document mutation in the store follows a four-line ritual — slice(0, historyIndex + 1), push cloneTree(newTree), set tree/history/historyIndex — repeated at roughly thirteen sites. toggleNode is the one that skips it.

Consequence

The invariant tree is structurally equal to history[historyIndex] is broken as soon as a node is toggled. Concretely: toggle a node off, then undo. Undo assigns tree = history[historyIndex - 1], a snapshot that predates the toggle and predates whatever edit came before it — so the toggle silently vanishes along with one more edit than the user asked to undo. Redo then reintroduces a state that never existed as such.

Disabling a node changes the rendered geometry and the exported mesh, so this is a document mutation, not view state. Compare toggleExpanded (:266) and setSelectedNode, which are genuinely view-only and correctly stay out of history.

Fix

Either push history in toggleNode like every other mutation, or — better, given thirteen hand-rolled copies — extract a commit(newTree) helper that does the slice/clone/push/set in one place and route every mutating action through it. That removes the class of bug rather than this instance.

immer is already a declared dependency and unused in this file; a helper could use it to avoid the JSON.parse(JSON.stringify(...)) deep clone at :75.

Related

While reading this: undo (:682) and redo (:690) restore only tree and historyIndex, not selectedNodeId. After undoing a node creation, selectedNodeId can dangle at a node no longer in the tree. Consumers mostly do findNode(...) and bail on null so it degrades quietly, but removeNode (:254) is the only action that clears a stale selection. Worth handling in the same change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions