Snapshots
A Snapshot (studio/core/document/src/document/history.rs) is what undo restores:
| Part | What it holds |
|---|---|
scene | the objects, their bodies and networks, the project settings and asset definitions |
location | where the user was inside an object; an object level is kept as the root |
objects | the objects selected |
picks | the nodes picked on the open level |
slots | every package's session slot values |
range | the frame range |
A snapshot costs a map of pointers. The scene is a map of shared objects, each object shares its body and its network, and the slot table holds one shared pointer a value. An edit copies only the object, network or slot value it writes, while the snapshots keep the versions they hold.
Where the user stands at an object level is navigation: a restore keeps the object level shown and puts back a place inside an object. The current time and playback are navigation too; the frame range is in the snapshot.
Steps
Document::recorded is the one place a step is recorded. It takes a snapshot, runs the change, takes a second snapshot and compares them: when they are equal nothing is recorded; otherwise the redo steps are dropped and a Step (its label, the snapshot before, the snapshot after, its bytes) goes onto the undo steps. Three methods record through it:
Document::transact, every edit of the scene (An edit becomes a frame follows one);Document::edit_slot, every change to a session slot;Document::set_time_range, the frame range.
A failed edit or check leaves the document as it was, so the two snapshots are equal and no step is recorded. The step's label is what Edit ▸ Undo and Redo show ("Undo Transform objects"), read with Document::undo_label and Document::redo_label.
Undo and redo
Document::undo first puts back an open gesture, whoever opened it, recording nothing. It then takes the newest step, restores its snapshot before, and moves the step onto the redo steps. Document::redo does the same with the newest redo step and restores its snapshot after: the scene as the edit left it, the location inside an object where the edit was made, the selection and picks the edit left, the slots and the frame range. A new step drops every redo step.
A restore (Document::restore):
- replaces the scene and moves the document's revisions,
Document::revisionamong them when a network differs, so the scene cook is asked for the restored scene; - puts back the place inside an object that the snapshot holds, while a snapshot taken at an object level keeps the object level shown now; when the restored scene lacks the place, it moves up to the nearest place the scene can show (
Location::kept,studio/core/document/src/document/location.rs); - puts back the selection and the picks, clearing the picks when their place is gone, and drops selected objects that are gone or locked;
- puts back the slots, then asks each slot to bring its value in line with the open level (
Slot::restored); - puts back the frame range, keeping the current time inside it.
Whether the document has unsaved changes is a comparison with what the file holds, so undoing back to the saved state leaves the document clean.
In the editor (kinogaki-editor), Undo and Redo first cancel the open context's live tools with Ended::Undo (studio/core/editor/src/frame/layer.rs), then call the document. A package that asks for an undo through the host has it queued until its own call returns. A drag whose gesture an undo ended ends with it (gesture::Held::live). After the restore, the cook finds the networks' compiled topologies kept (each network keeps its last two), so undoing and redoing the newest step compile nothing again (studio/core/network/tests/walk.rs).
History limits
The undo steps keep the newest 128 within a soft budget of 64 MB. A step's bytes are estimated once, when it is made: the objects that differ between its two sides (Scene::changed_bytes), both sides' slot values (Slot::estimated_bytes) and the label. The total is kept as steps come and go. When a new step passes either limit, the oldest steps go, and the newest step always stays. Starting or opening a document starts an empty history.
Gestures
A gesture makes every edit until it ends one step. Document::begin_gesture(label) opens one and returns its GestureToken (sdk/crates/api/src/session.rs), or None while a gesture is open. The token is the opener's alone and ends the gesture once: finishing or cancelling takes it by value, and it is the only way to do either. Its number comes from one count for the whole process, so a token held across a new document never names that document's gesture.
Inside a gesture, Document::recorded takes no snapshot; each edit is checked for what it changed, and nothing is recorded until the end.
Document::finish_gesture(token)checks the networks the gesture changed whole, once, and records one step named by the gesture's label (none when nothing changed). When the check fails, everything the gesture changed is put back and the error says why. A token whose gesture an undo already ended finishes nothing.Document::cancel_gesture(token)puts back everything the gesture changed and records nothing.Document::markmarks the document as it is now inside the open gesture and returns aGestureMark; a gesture keeps one mark, the newest.Document::back_to(mark)puts back everything changed since that mark, or since the start forGestureToken::start, and keeps the gesture open. A mark of a gesture that ended, or one a newer mark replaced, does nothing.
Packages reach these through HostUndo in kinogaki-studio-ui-api (sdk/crates/ui-api/src/host/capabilities.rs): begin_gesture, finish_gesture, cancel_gesture, back_to, in_gesture, undo, redo and the two labels. With back_to(token.start()), a live tool that computes each step from the gesture's start stays one undo step.
Drags and live tools
Every part of the editor that drags holds a gesture::Held (studio/core/editor/src/gesture.rs): the views (the gizmo, handles, the open context's selection, and a view steering a Camera object), the network pane's node moves, the outliner's flag drags, the timeline's frame range, the inspector's sheet and the canvas. A drag asks it to begin, finish (release or Enter) or cancel (Escape or focus lost):
- No gesture open. The drag opens one and holds its token (
Held::Own): its end finishes it as one step, and Escape cancels it. - Another's gesture open. The drag marks where it joined (
Held::Joined): its end leaves the gesture open, and Escape goes back to the mark. A part ends only a gesture it opened.
Mesh's live tools (plugins/mesh, plugins/mesh/src/ui/tools.rs) show how a run joins this. Running a tool opens a gesture named for the operation, adds the tool's node (or a step of the displayed Edit Mesh) and keeps the token. While the run lasts, its card's rows take the inspector's path, its handles are dragged and templated branches are picked into its node, each joining the tool's gesture. The card's Done, Enter, a click, running another tool, or going on to something else (the location leaving the context, a node picked or edited in the network) keeps the run and finishes the gesture as one step. The card's ×, Escape, Undo and Redo put the run back, and Undo or Redo then takes its step.
Selections, picks and slots
- Objects selected. Picking objects is no step (
Document::pick_object,Document::select_objects). Every snapshot carries the selection, so undo and redo restore the selection each step came with. An edit may leave a selection as part of its step (Transaction::select). - Nodes picked.
NodePicks(studio/core/document/src/document/picks.rs) holds the picks of the open level in pick order. Picking is navigation, restored with each step like the selection; an edit may pick, such as a new node (Transaction::pick). - Components selected. Core's own slot,
ComponentSelectionin kinogaki-core-package (studio/core/package/src/component_selection.rs), holds the components selected on one object, whatever its context. The host sets it with anedit_slotnamed "Select", so a component pick is an undo step, part of the open gesture's when one is open. - Slots. A package's session state is a value type implementing the SDK's
Slot(kinogaki-studio-api), registered withPackage::with_slot: its id, its initial value,restored(its value in line with where the user is after an edit, an undo, a redo, a cancelled gesture or a move to another level) andestimated_bytesfor the history's budget. The document keeps every installed slot in oneSessionSlotstable (studio/core/document/src/document/slots.rs), and every snapshot keeps the whole table, so every slot is undone with the document. A slot value is copied only when an edit changes it while a snapshot shares it, and a change that leaves the value equal records no step.
These stay out of undo: a package's UI state (the state type the host lends to each call of its UI parts); the object level shown, the current time and playback; each view's camera and display options, saved with the project's view (studio/core/editor/src/view/cameras.rs); the user's settings; and the network's stacking and its nodes' views (Document::raise_nodes, Document::set_node_view), saved with the document and recorded as no step, as is Reload (studio/core/document/src/document/edits/nodes.rs).
Held edits across snapshots
An Edit Mesh node (plugins/mesh/src/nodes/edit_mesh) holds its steps as a delta over its captured input. Core keeps the mechanism in kinogaki-cook and kinogaki-network: the node's HeldNode holds the delta and a Held state (studio/core/cook/src/held.rs), a base captured by the first cook that reaches the node and a result filled by the first cook that finishes. The network is shared by every snapshot that holds it, and so is the held state with its result.
- A new state for each change.
CoreNetwork::set_delta(studio/core/network/src/held_edits.rs) gives the nodeHeld::adjusted: the same captured base with a fresh result slot. A cook of an earlier state fills that state's slot alone. - Undo shows what it showed. The walker takes a held state's result when it has one and applies nothing (
studio/core/network/src/walk.rs). Undoing an Edit Mesh step restores the network holding the earlier state, whose result is the allocation shown before the step; redoing restores the step's own state, with the result its cook filled. - The rest follows by stamps. Nodes downstream of the held node cook as their stamps say (An edit becomes a frame).
