tokens-studio / graph-engine

Graph engine for resolvers and generators
Mozilla Public License 2.0
45 stars 7 forks source link

Add Undo/Redo #537

Open AlexBxl opened 6 days ago

AlexBxl commented 6 days ago

Overview

The idea is that everything happens through an Action, and there is an ActionManager 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 and next 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.

AlexBxl commented 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.

SorsOps commented 6 days ago

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

AlexBxl commented 6 days ago

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.

six7 commented 6 days ago

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?

AlexBxl commented 6 days ago

I think so, at the very least we could do what MS Office does.

image

SorsOps commented 5 days ago

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

https://www.npmjs.com/package/undo-manager