diff --git a/include/QtNodes/internal/AbstractGraphModel.hpp b/include/QtNodes/internal/AbstractGraphModel.hpp index 96d2c64b..71655011 100644 --- a/include/QtNodes/internal/AbstractGraphModel.hpp +++ b/include/QtNodes/internal/AbstractGraphModel.hpp @@ -1,16 +1,15 @@ #pragma once #include "Export.hpp" - -#include -#include +#include "ConnectionIdHash.hpp" +#include "Definitions.hpp" #include #include #include -#include "ConnectionIdHash.hpp" -#include "Definitions.hpp" +#include + namespace QtNodes { @@ -33,23 +32,23 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject /// @brief Returns the full set of unique Node Ids. /** - * Model creator is responsible for generating unique `unsigned int` - * Ids for all the nodes in the graph. From an Id it should be - * possible to trace back to the model's internal representation of - * the node. - */ + * Model creator is responsible for generating unique `unsigned int` + * Ids for all the nodes in the graph. From an Id it should be + * possible to trace back to the model's internal representation of + * the node. + */ virtual std::unordered_set allNodeIds() const = 0; /** - * A collection of all input and output connections for the given `nodeId`. - */ + * A collection of all input and output connections for the given `nodeId`. + */ virtual std::unordered_set allConnectionIds(NodeId const nodeId) const = 0; /// @brief Returns all connected Node Ids for given port. /** - * The returned set of nodes and port indices correspond to the type - * opposite to the given `portType`. - */ + * The returned set of nodes and port indices correspond to the type + * opposite to the given `portType`. + */ virtual std::unordered_set connections(NodeId nodeId, PortType portType, PortIndex index) const @@ -60,51 +59,52 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject /// Creates a new node instance in the derived class. /** - * The model is responsible for generating a unique `NodeId`. - * @param[in] nodeType is free to be used and interpreted by the - * model on its own, it helps to distinguish between possible node - * types and create a correct instance inside. - */ + * The model is responsible for generating a unique `NodeId`. + * @param[in] nodeType is free to be used and interpreted by the + * model on its own, it helps to distinguish between possible node + * types and create a correct instance inside. + */ virtual NodeId addNode(QString const nodeType = QString()) = 0; /// Model decides if a conection with a given connection Id possible. /** - * The default implementation compares corresponding data types. - * - * It is possible to override the function and connect non-equal - * data types. - */ + * The default implementation compares corresponding data types. + * + * It is possible to override the function and connect non-equal + * data types. + */ virtual bool connectionPossible(ConnectionId const connectionId) const = 0; /// Defines if detaching the connection is possible. virtual bool detachPossible(ConnectionId const) const { return true; } - /// Creates a new connection between two nodes. /** - * Default implementation emits signal - * `connectionCreated(connectionId)` - * - * In the derived classes user must emite the signal to notify the - * scene about the changes. - */ + * @brief Creates a new connection between two nodes. + * + * Default implementation emits signal + * `connectionCreated(connectionId)` + * + * In the derived classes user must emite the signal to notify the + * scene about the changes. + */ virtual void addConnection(ConnectionId const connectionId) = 0; /** - * @returns `true` if there is data in the model associated with the - * given `nodeId`. - */ + * @returns `true` if there is data in the model associated with the + * given `nodeId`. + */ virtual bool nodeExists(NodeId const nodeId) const = 0; /// @brief Returns node-related data for requested NodeRole. /** - * @returns Node Caption, Node Caption Visibility, Node Position etc. - */ + * @returns Node Caption, Node Caption Visibility, Node Position etc. + */ virtual QVariant nodeData(NodeId nodeId, NodeRole role) const = 0; /** - * A utility function that unwraps the `QVariant` value returned from the - * standard `QVariant AbstractGraphModel::nodeData(NodeId, NodeRole)` function. - */ + * A utility function that unwraps the `QVariant` value returned from the + * standard `QVariant AbstractGraphModel::nodeData(NodeId, NodeRole)` function. + */ template T nodeData(NodeId nodeId, NodeRole role) const { @@ -117,26 +117,28 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject return NodeFlag::NoFlags; } - /// @brief Sets node properties. /** - * Sets: Node Caption, Node Caption Visibility, - * Shyle, State, Node Position etc. - * @see NodeRole. - */ + * @brief Sets node properties. + * + * Sets: Node Caption, Node Caption Visibility, + * Shyle, State, Node Position etc. + * @see NodeRole. + */ virtual bool setNodeData(NodeId nodeId, NodeRole role, QVariant value) = 0; - /// @brief Returns port-related data for requested NodeRole. /** - * @returns Port Data Type, Port Data, Connection Policy, Port - * Caption. - */ + * @brief Returns port-related data for requested NodeRole. + * + * @returns Port Data Type, Port Data, Connection Policy, Port + * Caption. + */ virtual QVariant portData(NodeId nodeId, PortType portType, PortIndex index, PortRole role) const = 0; /** - * A utility function that unwraps the `QVariant` value returned from the - * standard `QVariant AbstractGraphModel::portData(...)` function. - */ + * A utility function that unwraps the `QVariant` value returned from the + * standard `QVariant AbstractGraphModel::portData(...)` function. + */ template T portData(NodeId nodeId, PortType portType, PortIndex index, PortRole role) const { @@ -155,74 +157,76 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject virtual bool deleteNode(NodeId const nodeId) = 0; /** - * Reimplement the function if you want to store/restore the node's - * inner state during undo/redo node deletion operations. - */ + * Reimplement the function if you want to store/restore the node's + * inner state during undo/redo node deletion operations. + */ virtual QJsonObject saveNode(NodeId const) const { return {}; } /** - * Reimplement the function if you want to support: - * - * - graph save/restore operations, - * - undo/redo operations after deleting the node. - * - * QJsonObject must contain following fields: - * - * - * ``` - * { - * id : 5, - * position : { x : 100, y : 200 }, - * internal-data { - * "your model specific data here" - * } - * } - * ``` - * - * The function must do almost exacly the same thing as the normal addNode(). - * The main difference is in a model-specific `inner-data` processing. - */ + * Reimplement the function if you want to support: + * + * - graph save/restore operations, + * - undo/redo operations after deleting the node. + * + * QJsonObject must contain following fields: + * + * + * ```json + * { + * id : 5, + * position : { x : 100, y : 200 }, + * internal-data { + * "your model specific data here" + * } + * } + * ``` + * + * The function must do almost exacly the same thing as the normal addNode(). + * The main difference is in a model-specific `inner-data` processing. + */ virtual void loadNode(QJsonObject const &) {} + virtual bool loopsEnabled() const { return true; } + public: /** - * Function clears connections attached to the ports that are scheduled to be - * deleted. It must be called right before the model removes its old port data. - * - * @param nodeId Defines the node to be modified - * @param portType Is either PortType::In or PortType::Out - * @param first Index of the first port to be removed - * @param last Index of the last port to be removed - */ + * Function clears connections attached to the ports that are scheduled to be + * deleted. It must be called right before the model removes its old port data. + * + * @param nodeId Defines the node to be modified + * @param portType Is either PortType::In or PortType::Out + * @param first Index of the first port to be removed + * @param last Index of the last port to be removed + */ void portsAboutToBeDeleted(NodeId const nodeId, PortType const portType, PortIndex const first, PortIndex const last); /** - * Signal emitted when model no longer has the old data associated with the - * given port indices and when the node must be repainted. - */ + * Signal emitted when model no longer has the old data associated with the + * given port indices and when the node must be repainted. + */ void portsDeleted(); /** - * Signal emitted when model is about to create new ports on the given node. - * @param first Is the first index of the new port after insertion. - * @param last Is the last index of the new port after insertion. - * - * Function caches existing connections that are located after the `last` port - * index. For such connections the new "post-insertion" addresses are computed - * and stored until the function AbstractGraphModel::portsInserted is called. - */ + * Signal emitted when model is about to create new ports on the given node. + * @param first Is the first index of the new port after insertion. + * @param last Is the last index of the new port after insertion. + * + * Function caches existing connections that are located after the `last` port + * index. For such connections the new "post-insertion" addresses are computed + * and stored until the function AbstractGraphModel::portsInserted is called. + */ void portsAboutToBeInserted(NodeId const nodeId, PortType const portType, PortIndex const first, PortIndex const last); /** - * Function re-creates the connections that were shifted during the port - * insertion. After that the node is updated. - */ + * Function re-creates the connections that were shifted during the port + * insertion. After that the node is updated. + */ void portsInserted(); Q_SIGNALS: diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index 26880020..ff93c6eb 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -43,6 +43,7 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel : public AbstractGraphModel, public NodeId addNode(QString const nodeType) override; + bool connectionPossible(ConnectionId const connectionId) const override; void addConnection(ConnectionId const connectionId) override; @@ -72,16 +73,19 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel : public AbstractGraphModel, public QJsonObject saveNode(NodeId const) const override; - QJsonObject save() const override; - void loadNode(QJsonObject const &nodeJson) override; + + // From Serializable + QJsonObject save() const override; + + // From Serializable void load(QJsonObject const &json) override; /** - * Fetches the NodeDelegateModel for the given `nodeId` and tries to cast the - * stored pointer to the given type - */ + * Fetches the NodeDelegateModel for the given `nodeId` and tries to cast the + * stored pointer to the given type + */ template NodeDelegateModelType *delegateModel(NodeId const nodeId) { @@ -94,6 +98,9 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel : public AbstractGraphModel, public return model; } + /// Loops do not make any sense in uni-direction data propagation + bool loopsEnabled() const override { return false; } + Q_SIGNALS: void inPortDataWasSet(NodeId const, PortType const, PortIndex const); @@ -106,15 +113,15 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel : public AbstractGraphModel, public private Q_SLOTS: /** - * Fuction is called in three cases: - * - * - By underlying NodeDelegateModel when a node has new data to propagate. - * @see DataFlowGraphModel::addNode - * - When a new connection is created. - * @see DataFlowGraphModel::addConnection - * - When a node restored from JSON an needs to send data downstream. - * @see DataFlowGraphModel::loadNode - */ + * Fuction is called in three cases: + * + * - By underlying NodeDelegateModel when a node has new data to propagate. + * @see DataFlowGraphModel::addNode + * - When a new connection is created. + * @see DataFlowGraphModel::addConnection + * - When a node restored from JSON an needs to send data downstream. + * @see DataFlowGraphModel::loadNode + */ void onOutPortDataUpdated(NodeId const nodeId, PortIndex const portIndex); /// Function is called after detaching a connection. diff --git a/include/QtNodes/internal/NodeConnectionInteraction.hpp b/include/QtNodes/internal/NodeConnectionInteraction.hpp index aa70a893..b22f3c7a 100644 --- a/include/QtNodes/internal/NodeConnectionInteraction.hpp +++ b/include/QtNodes/internal/NodeConnectionInteraction.hpp @@ -1,19 +1,18 @@ #pragma once -#include +#include "Definitions.hpp" #include -#include "Definitions.hpp" - namespace QtNodes { class ConnectionGraphicsObject; class NodeGraphicsObject; class BasicGraphicsScene; -/// Class wraps conecting and disconnecting checks. /** + * @brief Class wraps conecting and disconnecting checks. + * * An instance should be created on the stack and destroyed * automatically when the operation is completed */ @@ -25,11 +24,17 @@ class NodeConnectionInteraction BasicGraphicsScene &scene); /** + * @brief We check connection possibility from the perspecpive of + * ConnectionGraphicsObject first and just then ask the GraphModel + * * Can connect when following conditions are met: - * 1. Connection 'requires' a port. - * 2. Connection loose end is above the node port. - * 3. Source and target `nodeId`s are different. - * 4. GraphModel permits connection. + * 1. ConnectionGrachicsObject::connectionState() 'requires' a port. + * 2. Connection loose end is geometrically above the node port. + * 3. GraphModel permits connection + * - Here we check specific data type + * - multi-connection policy + * - New connection does not introduce a loop if + * `AbstractGrphModel::loopsEnabled()` forbits it. */ bool canConnect(PortIndex *portIndex) const; diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index ec2d21a4..f7892a02 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -1,10 +1,14 @@ #include "DataFlowGraphModel.hpp" + #include "ConnectionIdHash.hpp" +#include "Definitions.hpp" #include +#include #include + namespace QtNodes { DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr registry) @@ -142,13 +146,49 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con return connected.empty() || (policy == ConnectionPolicy::Many); }; - return getDataType(PortType::Out).id == getDataType(PortType::In).id + bool const basicChecks = + getDataType(PortType::Out).id == getDataType(PortType::In).id && portVacant(PortType::Out) && portVacant(PortType::In) && checkPortBounds(PortType::Out) && checkPortBounds(PortType::In); + + // In data-flow mode (this class) it's important to forbid graph loops. + // We perform depth-first graph traversal starting from the "Input" port of + // the given connection. We should never encounter the starting "Out" node. + + auto hasLoops = [this, &connectionId]() -> bool { + std::stack filo; + filo.push(connectionId.inNodeId); + + while (!filo.empty()) + { + auto id = filo.top(); filo.pop(); + + if (id == connectionId.outNodeId) { // LOOP! + return true; + } + + // Add out-connections to continue interations + std::size_t const nOutPorts = + nodeData(id, NodeRole::OutPortCount).toUInt(); + + for (PortIndex index = 0; index < nOutPorts; ++index) { + auto const &outConnectionIds = connections(id, PortType::Out, index); + + for (auto cid : outConnectionIds) { + filo.push(cid.inNodeId); + } + } + } + + return false; + }; + + return basicChecks && (loopsEnabled() || !hasLoops()); } + void DataFlowGraphModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); @@ -336,8 +376,9 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, switch (role) { case PortRole::Data: - if (portType == PortType::Out) + if (portType == PortType::Out) { result = QVariant::fromValue(model->outData(portIndex)); + } break; case PortRole::DataType: diff --git a/src/NodeConnectionInteraction.cpp b/src/NodeConnectionInteraction.cpp index f0f7c61c..c43fe36f 100644 --- a/src/NodeConnectionInteraction.cpp +++ b/src/NodeConnectionInteraction.cpp @@ -21,37 +21,25 @@ NodeConnectionInteraction::NodeConnectionInteraction(NodeGraphicsObject &ngo, , _scene(scene) {} + +// This is the chneck from the perspective of the ConnectionGraphicsObject bool NodeConnectionInteraction::canConnect(PortIndex *portIndex) const { // 1. Connection requires a port. - - PortType requiredPort = _cgo.connectionState().requiredPort(); - + PortType const requiredPort = _cgo.connectionState().requiredPort(); if (requiredPort == PortType::None) { return false; } - NodeId connectedNodeId = getNodeId(oppositePort(requiredPort), _cgo.connectionId()); - - // 2. Forbid connecting the node to itself. - - if (_ngo.nodeId() == connectedNodeId) - return false; - - // 3. Connection loose end is above the node port. - - QPointF connectionPoint = _cgo.sceneTransform().map(_cgo.endPoint(requiredPort)); - + // 2. Connection loose end is above the node port. + QPointF const connectionPoint = _cgo.sceneTransform().map(_cgo.endPoint(requiredPort)); *portIndex = nodePortIndexUnderScenePoint(requiredPort, connectionPoint); - if (*portIndex == InvalidPortIndex) { return false; } - // 4. Model allows connection. - + // 3. Model permits connection. AbstractGraphModel &model = _ngo.nodeScene()->graphModel(); - ConnectionId connectionId = makeCompleteConnectionId(_cgo.connectionId(), // incomplete _ngo.nodeId(), // missing node id *portIndex); // missing port index diff --git a/test/include/TestDataFlowNodes.hpp b/test/include/TestDataFlowNodes.hpp index 89fe8379..778290c4 100644 --- a/test/include/TestDataFlowNodes.hpp +++ b/test/include/TestDataFlowNodes.hpp @@ -6,8 +6,10 @@ #include #include #include + #include + using QtNodes::NodeData; using QtNodes::NodeDataType; using QtNodes::NodeDelegateModel; @@ -33,21 +35,43 @@ class TestData : public NodeData QString _text; }; + // Simple source node that outputs test data class TestSourceNode : public NodeDelegateModel { Q_OBJECT public: - TestSourceNode(); + TestSourceNode() + { + _lineEdit = new QLineEdit("Hello World"); + connect(_lineEdit, &QLineEdit::textChanged, this, &TestSourceNode::onTextChanged); + } QString caption() const override { return "Test Source"; } QString name() const override { return "TestSourceNode"; } static QString Name() { return "TestSourceNode"; } - unsigned int nPorts(PortType portType) const override; - NodeDataType dataType(PortType portType, PortIndex portIndex) const override; - std::shared_ptr outData(PortIndex const portIndex) override; + unsigned int nPorts(PortType portType) const override + { + return (portType == PortType::Out) ? 1 : 0; + } + + NodeDataType dataType(PortType portType, PortIndex portIndex) const override + { + Q_UNUSED(portIndex); + if (portType == PortType::Out) { + return TestData{}.type(); + } + return NodeDataType{}; + } + + std::shared_ptr outData(PortIndex const portIndex) override + { + Q_UNUSED(portIndex); + return std::make_shared(_lineEdit->text()); + } + void setInData(std::shared_ptr, PortIndex const) override {} QWidget* embeddedWidget() override { return _lineEdit; } @@ -56,34 +80,76 @@ class TestSourceNode : public NodeDelegateModel void setText(const QString& text) { _lineEdit->setText(text); } private Q_SLOTS: - void onTextChanged(); + void onTextChanged() + { + Q_EMIT dataUpdated(0); + } private: QLineEdit* _lineEdit; }; -// Simple display node that receives and shows test data + +// Simple display node that receives and shows test dataR +// And propagates it downstream class TestDisplayNode : public NodeDelegateModel { Q_OBJECT public: - TestDisplayNode(); + TestDisplayNode() + { + _label = new QLabel("No Data"); + } QString caption() const override { return "Test Display"; } QString name() const override { return "TestDisplayNode"; } static QString Name() { return "TestDisplayNode"; } - unsigned int nPorts(PortType portType) const override; - NodeDataType dataType(PortType portType, PortIndex portIndex) const override; - std::shared_ptr outData(PortIndex const portIndex) override; - void setInData(std::shared_ptr data, PortIndex const portIndex) override; + unsigned int nPorts(PortType portType) const override + { + return (portType == PortType::In) ? 1 : 1; + } - QWidget* embeddedWidget() override { return _label; } + NodeDataType dataType(PortType portType, PortIndex portIndex) const override + { + Q_UNUSED(portType); + Q_UNUSED(portIndex); + return TestData{}.type(); + } - QString getReceivedData() const { return _receivedData; } + std::shared_ptr outData(PortIndex const portIndex) override + { + Q_UNUSED(portIndex); + return _receivedData; + } + + void setInData(std::shared_ptr data, + PortIndex const portIndex) override + { + Q_UNUSED(portIndex); + auto d = std::dynamic_pointer_cast(data); + if (d) { + _receivedData = d; + _label->setText(d->text()); + } else { + _receivedData.reset(); + _label->setText("No Data"); + } + + // Propagate downstream + Q_EMIT dataUpdated(0); + } + + QWidget* embeddedWidget() override { return _label; } + QString getText() const { + if (_receivedData) { + return _receivedData->text(); + } + return {}; + } private: QLabel* _label; - QString _receivedData; + std::shared_ptr _receivedData; }; diff --git a/test/src/TestDataFlow.cpp b/test/src/TestDataFlow.cpp index ae1d7b99..95ec307b 100644 --- a/test/src/TestDataFlow.cpp +++ b/test/src/TestDataFlow.cpp @@ -10,6 +10,7 @@ #include #include + #include #include #include @@ -21,78 +22,7 @@ using QtNodes::DataFlowGraphModel; using QtNodes::GraphicsView; using QtNodes::NodeDelegateModelRegistry; using QtNodes::NodeGraphicsObject; -using QtNodes::ConnectionGraphicsObject; -using QtNodes::ConnectionPolicy; - -// Implementation of TestSourceNode -TestSourceNode::TestSourceNode() -{ - _lineEdit = new QLineEdit("Hello World"); - connect(_lineEdit, &QLineEdit::textChanged, this, &TestSourceNode::onTextChanged); -} - -unsigned int TestSourceNode::nPorts(PortType portType) const -{ - return (portType == PortType::Out) ? 1 : 0; -} - -NodeDataType TestSourceNode::dataType(PortType portType, PortIndex portIndex) const -{ - Q_UNUSED(portIndex); - if (portType == PortType::Out) { - return TestData{}.type(); - } - return NodeDataType{}; -} - -std::shared_ptr TestSourceNode::outData(PortIndex const portIndex) -{ - Q_UNUSED(portIndex); - return std::make_shared(_lineEdit->text()); -} - -void TestSourceNode::onTextChanged() -{ - Q_EMIT dataUpdated(0); -} -// Implementation of TestDisplayNode -TestDisplayNode::TestDisplayNode() -{ - _label = new QLabel("No Data"); -} - -unsigned int TestDisplayNode::nPorts(PortType portType) const -{ - return (portType == PortType::In) ? 1 : 0; -} - -NodeDataType TestDisplayNode::dataType(PortType portType, PortIndex portIndex) const -{ - Q_UNUSED(portIndex); - if (portType == PortType::In) { - return TestData{}.type(); - } - return NodeDataType{}; -} - -std::shared_ptr TestDisplayNode::outData(PortIndex const portIndex) -{ - Q_UNUSED(portIndex); - return nullptr; -} - -void TestDisplayNode::setInData(std::shared_ptr data, PortIndex const portIndex) -{ - Q_UNUSED(portIndex); - if (auto testData = std::dynamic_pointer_cast(data)) { - _receivedData = testData->text(); - _label->setText(_receivedData); - } else { - _receivedData = ""; - _label->setText("No Data"); - } -} std::shared_ptr createTestRegistry() { @@ -105,12 +35,12 @@ std::shared_ptr createTestRegistry() TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") { auto app = applicationSetup(); - + auto registry = createTestRegistry(); DataFlowGraphModel model(registry); DataFlowGraphicsScene scene(model); GraphicsView view(&scene); - + view.resize(800, 600); view.show(); REQUIRE(QTest::qWaitForWindowExposed(&view)); @@ -118,58 +48,71 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") SECTION("Programmatic connection and data transfer") { - // Create source and display nodes + // Create source, middle, and display nodes auto sourceNodeId = model.addNode("TestSourceNode"); + auto middleNodeId = model.addNode("TestDisplayNode"); auto displayNodeId = model.addNode("TestDisplayNode"); - + REQUIRE(sourceNodeId != QtNodes::InvalidNodeId); + REQUIRE(middleNodeId != QtNodes::InvalidNodeId); REQUIRE(displayNodeId != QtNodes::InvalidNodeId); - - // Position the nodes + + // Position the nodes (that acually does not matter here) model.setNodeData(sourceNodeId, QtNodes::NodeRole::Position, QPointF(100, 100)); - model.setNodeData(displayNodeId, QtNodes::NodeRole::Position, QPointF(300, 100)); + model.setNodeData(middleNodeId, QtNodes::NodeRole::Position, QPointF(300, 300)); + model.setNodeData(displayNodeId, QtNodes::NodeRole::Position, QPointF(500, 500)); UITestHelper::waitForUI(); // Get the delegate models to access their functionality auto sourceModel = model.delegateModel(sourceNodeId); + auto middleModel = model.delegateModel(middleNodeId); auto displayModel = model.delegateModel(displayNodeId); - + REQUIRE(sourceModel != nullptr); + REQUIRE(middleModel != nullptr); REQUIRE(displayModel != nullptr); // Verify initial state QString initialText = "Test Data Transfer"; sourceModel->setText(initialText); UITestHelper::waitForUI(); - + CHECK(sourceModel->getCurrentText() == initialText); - CHECK(displayModel->getReceivedData() == ""); // No connection yet + CHECK(displayModel->getText() == ""); // No connection yet - // Create connection programmatically - QtNodes::ConnectionId connectionId{sourceNodeId, 0, displayNodeId, 0}; - model.addConnection(connectionId); + // Create first connection programmatically + QtNodes::ConnectionId connectionId1{sourceNodeId, 0, middleNodeId, 0}; + model.addConnection(connectionId1); UITestHelper::waitForUI(); - // Verify data was transferred through the connection - CHECK(displayModel->getReceivedData() == initialText); - + CHECK(middleModel->getText() == initialText); + + // Create second connection programmatically + QtNodes::ConnectionId connectionId2{middleNodeId, 0, displayNodeId, 0}; + model.addConnection(connectionId2); + UITestHelper::waitForUI(); + + // Verify data was transferred through the connections + CHECK(displayModel->getText() == initialText); + // Test that data updates propagate QString newText = "Updated Data"; sourceModel->setText(newText); UITestHelper::waitForUI(); - - CHECK(displayModel->getReceivedData() == newText); - + + CHECK(displayModel->getText() == newText); + // Test disconnection stops data flow - model.deleteConnection(connectionId); + model.deleteConnection(connectionId1); UITestHelper::waitForUI(); - + // Change source data after disconnection sourceModel->setText("Should Not Transfer"); UITestHelper::waitForUI(); - - // After disconnection, display should have empty data (framework sends null data to disconnected nodes) - CHECK(displayModel->getReceivedData() == ""); + + // After disconnection, display should have empty data + // (framework sends null data to disconnected nodes) + CHECK(displayModel->getText() == ""); } SECTION("Interactive connection creation and data transfer") @@ -177,14 +120,14 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") // Create source and display nodes auto sourceNodeId = model.addNode("TestSourceNode"); auto displayNodeId = model.addNode("TestDisplayNode"); - + model.setNodeData(sourceNodeId, QtNodes::NodeRole::Position, QPointF(100, 100)); model.setNodeData(displayNodeId, QtNodes::NodeRole::Position, QPointF(350, 100)); - + // Set initial data auto sourceModel = model.delegateModel(sourceNodeId); auto displayModel = model.delegateModel(displayNodeId); - + QString testData = "Interactive Test"; sourceModel->setText(testData); UITestHelper::waitForUI(); @@ -197,7 +140,7 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") // Find the node graphics objects NodeGraphicsObject* sourceGraphics = nullptr; NodeGraphicsObject* displayGraphics = nullptr; - + for (auto item : scene.items()) { if (auto node = qgraphicsitem_cast(item)) { QPointF nodePos = node->pos(); @@ -208,14 +151,14 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") } } } - + REQUIRE(sourceGraphics != nullptr); REQUIRE(displayGraphics != nullptr); // Calculate port positions for connection QRectF sourceBounds = sourceGraphics->boundingRect(); QRectF displayBounds = displayGraphics->boundingRect(); - + QPointF outputPortPos = sourceGraphics->mapToScene( QPointF(sourceBounds.right() - 5, sourceBounds.center().y()) ); @@ -227,7 +170,7 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") QSignalSpy connectionSpy(&model, &DataFlowGraphModel::connectionCreated); // Verify no initial data transfer - CHECK(displayModel->getReceivedData() == ""); + CHECK(displayModel->getText() == ""); // Simulate mouse drag to create connection UITestHelper::simulateMouseDrag(&view, outputPortPos, inputPortPos); @@ -237,11 +180,11 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") auto connections = model.allConnectionIds(sourceNodeId); INFO("Connections created: " << connections.size()); INFO("Connection signals: " << connectionSpy.count()); - + // In a successful connection, data should transfer if (connections.size() > 0) { - CHECK(displayModel->getReceivedData() == testData); - INFO("Data successfully transferred: " << displayModel->getReceivedData().toStdString()); + CHECK(displayModel->getText() == testData); + INFO("Data successfully transferred: " << displayModel->getText().toStdString()); } else { INFO("No connection created by mouse interaction - this may be expected depending on exact port hit testing"); } @@ -255,12 +198,12 @@ TEST_CASE("Data Flow - Basic Data Transfer", "[dataflow][visual]") TEST_CASE("Data Flow - Multiple Connections", "[dataflow][visual]") { auto app = applicationSetup(); - + auto registry = createTestRegistry(); DataFlowGraphModel model(registry); DataFlowGraphicsScene scene(model); GraphicsView view(&scene); - + view.resize(800, 600); view.show(); UITestHelper::waitForUI(); @@ -271,15 +214,15 @@ TEST_CASE("Data Flow - Multiple Connections", "[dataflow][visual]") auto sourceNodeId = model.addNode("TestSourceNode"); auto display1NodeId = model.addNode("TestDisplayNode"); auto display2NodeId = model.addNode("TestDisplayNode"); - + model.setNodeData(sourceNodeId, QtNodes::NodeRole::Position, QPointF(100, 100)); model.setNodeData(display1NodeId, QtNodes::NodeRole::Position, QPointF(300, 50)); model.setNodeData(display2NodeId, QtNodes::NodeRole::Position, QPointF(300, 150)); - + auto sourceModel = model.delegateModel(sourceNodeId); auto display1Model = model.delegateModel(display1NodeId); auto display2Model = model.delegateModel(display2NodeId); - + // Set test data QString testData = "Broadcast Data"; sourceModel->setText(testData); @@ -288,33 +231,33 @@ TEST_CASE("Data Flow - Multiple Connections", "[dataflow][visual]") // Create connections to both displays QtNodes::ConnectionId connection1{sourceNodeId, 0, display1NodeId, 0}; QtNodes::ConnectionId connection2{sourceNodeId, 0, display2NodeId, 0}; - + model.addConnection(connection1); model.addConnection(connection2); UITestHelper::waitForUI(); // Verify both displays received the data - CHECK(display1Model->getReceivedData() == testData); - CHECK(display2Model->getReceivedData() == testData); - + CHECK(display1Model->getText() == testData); + CHECK(display2Model->getText() == testData); + // Test that updates propagate to both QString newData = "Updated Broadcast"; sourceModel->setText(newData); UITestHelper::waitForUI(); - - CHECK(display1Model->getReceivedData() == newData); - CHECK(display2Model->getReceivedData() == newData); - + + CHECK(display1Model->getText() == newData); + CHECK(display2Model->getText() == newData); + // Test partial disconnection model.deleteConnection(connection1); UITestHelper::waitForUI(); - + sourceModel->setText("Only Display2"); UITestHelper::waitForUI(); - + // After disconnection, display1 should have empty data (disconnected nodes get null data) // Only display2 should get the new data (still connected) - CHECK(display1Model->getReceivedData() == ""); // Disconnected = empty data - CHECK(display2Model->getReceivedData() == "Only Display2"); // Gets new data + CHECK(display1Model->getText() == ""); // Disconnected = empty data + CHECK(display2Model->getText() == "Only Display2"); // Gets new data } } diff --git a/test/src/TestDataFlowGraphModel.cpp b/test/src/TestDataFlowGraphModel.cpp index a3ca5c3e..db0f7891 100644 --- a/test/src/TestDataFlowGraphModel.cpp +++ b/test/src/TestDataFlowGraphModel.cpp @@ -113,6 +113,29 @@ TEST_CASE("DataFlowGraphModel connections", "[dataflow]") ConnectionId invalidIn{node1, 0, node2, 2}; CHECK_FALSE(model.connectionPossible(invalidIn)); } + + SECTION("Loop connection between three nodes") + { + NodeId node3 = model.addNode("TestNode"); + + ConnectionId connId12{node1, 0, node2, 0}; + + CHECK(model.connectionPossible(connId12)); + model.addConnection(connId12); + CHECK(model.connectionExists(connId12)); + + ConnectionId connId23{node2, 0, node3, 0}; + + CHECK(model.connectionPossible(connId23)); + model.addConnection(connId23); + CHECK(model.connectionExists(connId23)); + + ConnectionId connId31{node3, 0, node1, 0}; + + CHECK_FALSE(model.connectionPossible(connId31)); + model.addConnection(connId31); + CHECK(model.connectionExists(connId31)); + } } TEST_CASE("DataFlowGraphModel serialization support", "[dataflow]")