retejs / rete

JavaScript framework for visual programming
https://retejs.org
MIT License
9.86k stars 647 forks source link

Recursion detected #255

Closed petrkrejci1 closed 5 years ago

petrkrejci1 commented 5 years ago

Hello, to extend this following answer with a question:

  1. At the moment, recurrent connections are forbidden, as this leads to hangup when processing nodes. Probably, in the future the question with cyclic execution of the nodes will be solved

_Originally posted by @Ni55aN in https://github.com/retejs/rete/issues/116#issuecomment-

...You wrote that recurrent connections are forbidden, but when I actually try to connect nodes in recursion it does not stop me. I am able to make recurrent connections, only thing I am getting is console log message saying following: "{message: "Recursion detected", data: Array(3)}".

Is there a way how to stop user from drawing the connection that creating recursive loop?

Thank you so much in advance and especially thank you for great Rete :-).

Ni55aN commented 5 years ago

You wrote that recurrent connections are forbidden, but when I actually try to connect nodes in recursion it does not stop me.

Forbidden for processing, but not for visualization

There is implementation based on imported data (toJSON()) https://github.com/retejs/rete/blob/master/src/engine/index.js#L49

by the same principle, a plugin can define recursion by the list of nodes in the editor

petrkrejci1 commented 5 years ago

by the same principle, a plugin can define recursion by the list of nodes in the editor

I see, so the question is from where is it the best to call this detectRecursions() function. I am thinking about calling it from (or maybe actually copy pasting it to) the picker.js located in rete-connection-plugin. Do you thing it is the good idea or would you rather suggest calling it from different place?

My aim is to stop user from being able to make a recursive connection in editor.

Thank you once again for your help. Cheers

Ni55aN commented 5 years ago

Do you thing it is the good idea or would you rather suggest calling it from different place?

Core of the framewok is event based, so it'll be reasonable to subscribe on connectioncreated event and detect recursion. I'm working on it now

Ni55aN commented 5 years ago

Fast solution, but not performant:

   engine.detectRecursions(editor.toJSON().nodes);
Ni55aN commented 5 years ago

This is not the best option, but quite working. It can be improved by subscribing to the connectioncreate event and returning false if the connection (not yet added) can lead to recursion (but it can be more difficult to implement

https://codepen.io/Ni55aN/pen/pGYrgp


const RecursionPlugin = {
      extractInputNodes(node) {
        return node.getConnections().filter(c => c.input.node === node).map(c => c.output.node);
      },
      detect(editor) {
        // console.log(window.engine.detectRecursions(editor.toJSON().nodes));
        const nodesArr = [...editor.nodes];
        const findSelf = (node, inputNodes) => {
            if (inputNodes.some(n => n === node))
                return node;

            for (var i = 0; i < inputNodes.length; i++) {
                if (findSelf(node, this.extractInputNodes(inputNodes[i])))
                    return node;
            }

            return null;
        }

        return nodesArr.map(node => {
            return findSelf(node, this.extractInputNodes(node))
        }).filter(r => r !== null);
    }

}
  editor.on('connectioncreated', (c) => {
    const recurrectNodes = RecursionPlugin.detect(editor);

    if(recurrectNodes.length > 0) {
      editor.removeConnection(c);
      alert('Connection removed due to recursion')
    }
  });
petrkrejci1 commented 5 years ago

Fast solution, but not performant:

   engine.detectRecursions(editor.toJSON().nodes);

This definitely works! The only problem I encountered is when I hit redo (Ctrl+Y) after trying to create recursive connection. It tries to recreate this recursive connection which is not appropriate behaviour I would say. But that is another thing, it would be probable good idea to not saving / removing this step from history stack.

Ni55aN commented 5 years ago

I think the ideal solution is detect recursion before adding a new connection (on connectioncreate)

petrkrejci1 commented 5 years ago

This is not the best option, but quite working. It can be improved by subscribing to the connectioncreate event and returning false if the connection (not yet added) can lead to recursion (but it can be more difficult to implement

https://codepen.io/Ni55aN/pen/pGYrgp

Just tried and yes, it is working as well. Probable good idea to add this with async function and await engine.abort() though.

One last thing I found annoying is when I am trying to create recursive connection while there is already another existing connection on the nodes's input (not the recursive one), it shows me an alert, but also remove the existing connection which is not necessary. So I agree with this statement for sure: https://github.com/retejs/rete/issues/255#issuecomment-465321324

Ni55aN commented 5 years ago

but also remove the existing connection which is not necessary

Before add a new connection, place will be allocated for it

https://github.com/retejs/connection-plugin/blob/master/src/picker.js#L39

Ni55aN commented 5 years ago

Improved recursion detector plugin based on connectioncreate event


const RecursionPlugin = {
    install(editor) {
      editor.on('connectioncreate', (c) => {
        const recurrectNode = RecursionPlugin.detect(c);

        if(recurrectNode) {
          alert('Connection removed due to recursion');
          return false;
        }
      });
    },
      findSelf (node, inputNodes){
        if (inputNodes.some(n => n === node))
          return true;

        for (var i = 0; i < inputNodes.length; i++) {
          if (this.findSelf(node, this.extractInputNodes(inputNodes[i])))
            return true;
        }

        return false;
      },
      extractInputNodes(node) {
        return node.getConnections().filter(c => c.input.node === node).map(c => c.output.node);
      },
      detect(inboundСonnection) {
        // console.log(window.engine.detectRecursions(editor.toJSON().nodes));
        const { input, output } = inboundСonnection;

        return input.node === output.node || this.findSelf(input.node, this.extractInputNodes(output.node));
    }
}
editor.use(RecursionPlugin);

But removed connection before connect() is lost

petrkrejci1 commented 5 years ago
        const { input, output } = inboundСonnection;

        return input.node === output.node || this.findSelf(input.node, this.extractInputNodes(output.node));

Regarding of node self connection (output -> input of the same node is connected). I used another approach that does not allows creating connection at all thus there is no removing of the existing one on the input (in case of input.multipleConnections = false).

Here is how I achieve it in picker.js:

  pickInput(input) {
    let isRecursiveNodeConnection = false;

    if (this.output != undefined)
    isRecursiveNodeConnection = input.node.id == this.output.node.id ? true : false;

    if (this.output === null) {
      if (input.hasConnection()) {
        this.output = input.connections[0].output;
        this.editor.removeConnection(input.connections[0]);
      }
      return true;
    }

    if (!input.multipleConnections && input.hasConnection() && !isRecursiveNodeConnection)
      this.editor.removeConnection(input.connections[0]);

    if (!this.output.multipleConnections && this.output.hasConnection())
      this.editor.removeConnection(this.output.connections[0]);

    if (this.output.connectedTo(input)) {
      var connection = input.connections.find(c => c.output === this.output);

      this.editor.removeConnection(connection);
    }

    if (!isRecursiveNodeConnection) {
      this.editor.connect(this.output, input);
      this.reset();
    }
  }

But the problem still remains for the case when connecting to different node that already has connection on it's input.

Ni55aN commented 5 years ago

@petrkrejci1 will the recursion be correctly defined when the old connection has not yet been deleted?

petrkrejci1 commented 5 years ago

@petrkrejci1 will the recursion be correctly defined when the old connection has not yet been deleted?

Yes, it will be detected before the connection is made by following condition:

if (this.output != undefined) isRecursiveNodeConnection = input.node.id == this.output.node.id ? true : false;

But this works only for case when connecting node to itself. Connections among other nodes are bit trickier. It is great that we checking recursion on 'connectioncreate' event in RecursionPlugin you wrote, but we also need to somehow suppress clicking on input of the node when trying to creating recursive connection. If we skip event of clicking on the node's input, we will be able to cancel removing of existing connections that is not recursive and should stay in place.

I honestly do not know how to achieve it. So here are pictures to show the case I am thinking about. Step 1: https://drive.google.com/open?id=1mgukXhfenzyro22lgoNp6DNJxzDQFATS Step 2: https://drive.google.com/open?id=1dBbuczP5HXscJNdDYRh2U5XqOHD5CK6k Step 3: https://drive.google.com/open?id=16A5qbrbhR2OLZ708me-asKCzXvbtp-Xn

... in step 3 you can see what I mean. Missing connection that should not be missing (noted in red).

@Ni55aN Do you thing we can do something about this issue? I am clueless to be honest.

Ni55aN commented 5 years ago

@petrkrejci1 you can save last connection on connectionremoved and restore it. But in some cases, false positives may occur due to manual removal of the connection.

Ni55aN commented 5 years ago

in v1.2.0-rc.1 you can import the Recursion class and export nodes to it

Ni55aN commented 5 years ago

More compact code:

const RecursionPlugin = {
    install(editor) {
      editor.on('connectioncreate', (c) => {
        const nodes = editor.toJSON().nodes;
        nodes[c.input.node.id].inputs[c.input.key].connections.push({ node: c.output.node.id, output: c.output.key })

        const recurrectNode = new Rete.Recursion(nodes).detect();

        if(recurrectNode) {
          alert('Connection removed due to recursion');
          return false;
        }
      });
    }
}

with restoring connection (and little hack - setTimeout)

const RecursionPlugin = {
    install(editor) {
      let lastConnection = null;

      editor.on('connectionremoved', c => {
        lastConnection = c;
        setTimeout(() => lastConnection = null, 0);
      });

      editor.on('connectioncreate', (c) => {
        const nodes = editor.toJSON().nodes;
        nodes[c.input.node.id].inputs[c.input.key].connections.push({ node: c.output.node.id, output: c.output.key })

        const recurrectNode = new Rete.Recursion(nodes).detect();

        if(recurrectNode) {
          alert('Connection removed due to recursion');
          if(lastConnection)
            editor.connect(lastConnection.output, lastConnection.input);
          return false;
        }
      });
    }
}
petrkrejci1 commented 5 years ago

Thank you so much for your help and for this wonderful solution.