Open AlexBxl opened 6 days ago
Something like this:
class Action { ... };
class MoveNodesAction extends Action
{
nodes;
diffX;
diffY;
constructor(nodes, diffX, diffY) {
this.nodes = nodes;
this.newDiffX = newDiffX;
this.newDiffY = newDiffY;
}
do() {
this.nodes.forEach(node => {
node.X += this.diffX;
node.Y += this.diffY;
};
}
undo() {
this.nodes.forEach(node => {
node.X -= this.diffX;
node.Y -= this.diffY;
};
}
redo() {
do(); // but sometimes redo can be optimized
}
}
class ActionManager
{
undo = [];
redo = [];
do(action)
{
action.do();
this.undo.push(action);
}
undo()
{
const action = this.undo.pop();
action.undo();
this.redo.push(action);
}
redo(action)
{
const action = this.redo.pop();
action.redo();
this.undo.push(action);
}
}
ActionManager actionManager;
...
'pointerup', e => {
actionManager.do(
new MoveNodesAction(selectedNodes, diffX, diffY)
)
}
The actual implementation of this action should store and set absolute coordinates per node, then the same action can be used for things like auto-layout, which should also happen through an action.
I agree completely, the action paradigm is established, we know it works and its a clear way to handle this implementation, I am adding a few notes here
Of particular importance here is side effects. For the most part nodes are supposed to be pure as much as possible to prevent side effects and storing intermediate state however the internals of other libraries are where we might face challenges. We already know for example that the image-magick-wasm library is weird in how it distributes data and writes to hidden properties on the object that we don't have access to. We just need to ensure moving forward that we try keep to libraries that either don't store state as much as possible, don't use global state at all , or have clear ways that we can replay data. Regarding that cloning subgraphs is currently an impure action with how it creates new node ids as this is currently completely random uuids.
Another thing is the layer at which actions are occuring. This is obviously related to the UI with the current implementation, but we have toyed with the notion of recording a controlflow graphs changes changes when it is placed in a run state and then being able to step through the resulting timelines. This needs to be clear from user made changes in the UI space for future reference.
Last thing to note is how this might work with some thing like Y.js in the future where again there might be individual users changes along with other users in a multiplayer setting, this would likely then be transmitting the change as an update to an action compatible Y.js array, thus we need to bear in mind that all actions need to be serializable to POJOs
We can make the decision to put all the action logic inside the actions, then nodes can stay as they are. In fact, maybe we can combine this with copy/paste logic. For example, there would be a layer that de/serializes the nodes and connections to JSON in a convenient way, and we could use that same format to store any weird properties of nodes in the undo, which would naturally extend into saving the undo with the graph.
Love this ❤
Would that mean we could technically also visualize the log in some way to the user? Like Figma does in their Version history?
I think so, at the very least we could do what MS Office does.
I've closed the previous ticket we had for this, as I think this supercedes it. I am copying this from the old ticket however, which was a candidate library to handle undo redo for us
Overview
The idea is that everything happens through an
Action
, and there is anActionManager
that handles everything. UI actions like moving nodes, and more substantive things like value changes, become a single action flow.In some cases actions will mostly just forward the request to nodes/connections/etc to do the actual work, but sometimes the logic will reside in the actions themselves, like in the example below.
Linking actions
There are situations where actions can be combinations of smaller actions, for example
Either things like Select & move can be separate actions that duplicate the code from Select and Move, because the combination should be optimized in some non-trivial way, or actions can have
prev
andnext
members that are either null, or form short linked lists, in which case the ActionManager can undo/redo the entire list.Connections
Connecting/disconnecting/replacing should also be done through actions. Connection diffs will need to be saved for things like deleting nodes/replacing connections. If there are situations where input value changes can cause a node to change the number of outputs, then connection diffs will have to be stored for value changes also, or even for all actions regardless.
Cooperating with the browser
Text inputs have their own undos. Here we have options:
Subgraphs
Either subgraph breadcrumb navigation is also an action, so that undo records the entire uninterrupted workflow, or it's not an action, in which case each graph/subgraph can maintain its own ActionManager.
Continuous changes
For things like dragging the mouse to smoothly change values, editing curves, (potentially) editing text, or anything that can change continuously in real time while a decision is being made, timing mechanisms and optimal intervals will need to be worked out. These can be slightly different for each situation.
Saving undo with the graph
The process of creating a node graph is visual and, just like with web browsing, how we get to a page can be an important part of the context. Saving undo with the graph could be an option in the settings, because after hours of connecting and re-connecting, then taking a break, you might come back and want to go back to "how you did it before the break" because you just had an idea. Snapshots and auto-save aside, this can be useful, but will require extra work to make undo serializable.
Multiplayer
Here the spectrum of options is from putting up an ownership wall to per-user-per-node changes that are somehow merged together.