'How to implement connections between nodes in a node graph?

I'm trying to make a node graph editor in Qt and c++ (I'm kinda unfamiliar with it). There are so called nodes that act similar to functions: they take some inputs, process them and return some outputs. These inputs and outputs are represented as ports that can be connected together with edges.

Each type of node is a separate class that inherits from the Node base that implements some functionality for connecting the nodes together and executing them.

So I need a way to pass one node's outputs of some arbitrary type to some other node's inputs through a common interface that the base class provides. And I also need to somehow restrict which nodes can be connected together based on the type of the data that gets passed while providing an option to convert one type to another.

Here's why I'm asking this question:

In my current implementation there is an AbstractNodeData class that is passed by reference between nodes and gets downcasted to a specific type when the actual data needs to be accessed. Each node has a QVector (very similar to std::vector) which holds output Ports of the node and a QVector of input Ports. A Port contains some information about its parent Node, its capacity (how many connections it can have) and a QVector of Connection objects. A Connection in its turn holds a pointer to the Node and the index of the port it's connected to. When a node is executed it accesses its input ports and calls getData(int connectionIndex) on them. The method then calls the getData() method on the appropriate Connection which then calls a third getData(int portIndex) on a Node which finally returns a pointer to AbstractNodeData.

And this is where I started to have a lot of concerns. Here's how my thought process went: It seems like it would be appropriate to store a pointer to a Port in the Connection object instead of a pointer to a Node and an index. I could also subclass Port to perform the type check and conversion in a virtual method. But that would be a fourth getData(). Also, I already have some (unnecessary?) indirection with the AbstractNodeData being passed by reference and polymorphic Ports would mean more pointers and indirection and even dynamic allocation if I don't store the ports as members of the nodes. Then, input and output ports have very similar structure and function, except for one thing: input nodes access other nodes' data and output nodes provide that data to input ports so it makes sense to have an InputPort class and an OutputPort class that derived from Port. But if I'm going to subclass ports for each data type that would mean that I would have to provide two classes for each type. Well, to eliminate the fourth getData() I could store a pointer to the node's output AbstractNodeData in Port. But wouldn't this violate the encapsulation principle?

Are my concerns valid? And if so, are there any better ways of implementing connections between nodes?

Here's the code I've described (messy and unfinished):

//Node.hpp
#pragma once

#include <QObject>
#include <QUuid>

#include "NodeData.hpp"
#include "Port.hpp"

class InputPort;
class OutputPort;

class Node : QObject
{
    Q_OBJECT
public:
    virtual AbstractNodeData* getData(int outputIndex) = 0;
    virtual void execute() = 0;
protected:
    QVector<Port*> inputs;
    QVector<Port*> outputs;
private:
    QUuid uuid;
};

//Port.hpp
#pragma once

#include <QVector>

#include "Node.hpp"
#include "Connection.hpp"
#include "NodeData.hpp"

class Connection;
class Node;

class Port
{

public:
    Port(Node *node, int index, int capacity = 1);
    int getConnectionCount();
    Connection getConnection(int connectionIndex);
    virtual bool acceptsConnection();
protected:
    Node* parentNode;
    int index;
    int capacity;  
    QVector<Connection> connections;
};

class InputPort : Port
{
    AbstractNodeData* getData(int connectionIndex);
};

class OutputPort : Port
{
    AbstractNodeData* getData();
};

//Connection.hpp
#pragma once

#include <QUuid>

#include "Node.hpp"
#include "NodeData.hpp"

class Node;

class Connection
{
public:
    Connection(Node*, int);
    
    Node* getNode();
    int getPortIndex();
    
    AbstractNodeData* getData();
    
private:
    Node *node;
    int portIndex;
};


//NodeData.hpp
#pragma once

class AbstractNodeData
{
public:
    virtual int getTypeID() const = 0;
    template <typename castType>
    castType* as(){
        return dynamic_cast<castType*>(this);
    }
};

template <typename dataType>
class NodeData
{
public:
    virtual dataType get() const = 0;
    virtual void set(dataType type) const = 0;
};

EDIT: Forgot to mention. I've looked at this repo's source code and I even borrowed some design decisions from it like the abstract node data but I think the way it's doing things is unnecessarily complicated.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source