diff --git a/include/QtNodes/internal/BasicGraphicsScene.hpp b/include/QtNodes/internal/BasicGraphicsScene.hpp index 64b11d9a..30e83acf 100644 --- a/include/QtNodes/internal/BasicGraphicsScene.hpp +++ b/include/QtNodes/internal/BasicGraphicsScene.hpp @@ -138,7 +138,7 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene * between old and new nodes. */ std::pair, std::unordered_map> restoreGroup( - QJsonObject const &groupJson); + QJsonObject const &groupJson, QHash const &nodeById); /** * @brief Returns a const reference to the mapping of existing groups. @@ -182,6 +182,46 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene */ void removeNodeFromGroup(NodeId nodeId); + /** + * @brief Loads serialized item (nodes, connections and groups) into the scene. + * @param data Serialized scene payload. + * @param pastePos Reference position used when pasting content. + * @param usePastePos When true, places the loaded content relative to pastePos position. + * @return Mapping between original node UUIDs and newly created node UUIDs. + */ + std::unordered_map loadItems(const QByteArray &data, + QPointF pastePos, + bool usePastePos = true); + + /** + * @brief Loads scene data from memory. + * @param data Serialized scene payload. + * @return Mapping between original node UUIDs and newly created node UUIDs. + */ + std::unordered_map loadFromMemory(const QByteArray &data); + + /** + * @brief Encodes NodeId into a QUuid representation. + * @param nodeId Node identifier. + * @return QUuid carrying the binary value of nodeId. + */ + QUuid encodeNodeId(NodeId nodeId); + + /** + * @brief Decodes a node QUuid into a NodeId. + * @param uuid QUuid containing an encoded node id. + * @return Decoded NodeId. + */ + NodeId decodeNodeUuid(QUuid const &uuid); + + /** + * @brief Converts a QUuid-to-QUuid map into a NodeId-to-NodeId map. + * @param uuidMap Map containing encoded old and new node QUuid pairs. + * @return Map with decoded NodeId pairs, excluding invalid entries. + */ + + std::unordered_map convertMap(std::unordered_map const &uuidMap); + public: /** * @returns NodeGraphicsObject associated with the given nodeId. diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index 445c90c0..55dc5a58 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -46,6 +46,8 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView double getScale() const; + BasicGraphicsScene *getNodeScene() { return nodeScene(); } + public Q_SLOTS: void scaleUp(); diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index adf92827..4b299863 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -578,6 +578,146 @@ void BasicGraphicsScene::removeNodeFromGroup(NodeId nodeId) nodeIt->second->lock(false); } +std::unordered_map BasicGraphicsScene::loadItems(const QByteArray &data, + QPointF pastePos, + bool usePastePos) +{ + QJsonObject const jsonDocument = QJsonDocument::fromJson(data).object(); + + std::unordered_map IDMap{}; + + QPointF offset; + bool offsetInitialized{false}; + clearSelection(); + + QJsonArray nodesJsonArray = jsonDocument.value("nodes").toArray(); + QHash nodeById; + nodeById.reserve(nodesJsonArray.size()); + for (const QJsonValue &v : nodesJsonArray) { + QJsonObject o = v.toObject(); + NodeId id = static_cast(o.value("id").toInt(InvalidNodeId)); + if (id != InvalidNodeId) + nodeById.insert(id, o); + } + + QSet createdOldNodeIds; + + QJsonArray groupsJsonArray = jsonDocument.value("groups").toArray(); + for (const QJsonValue &groupVal : groupsJsonArray) { + auto [groupWeakPtr, groupIDsMap] = restoreGroup(groupVal.toObject(), nodeById); + + for (const auto &[oldGroupId, newGroupId] : groupIDsMap) { + NodeId oldNodeId = static_cast(oldGroupId); + NodeId newNodeId = static_cast(newGroupId); + + createdOldNodeIds.insert(oldNodeId); + + QUuid oldUuid = encodeNodeId(oldNodeId); + QUuid newUuid = encodeNodeId(newNodeId); + IDMap[oldUuid] = newUuid; + } + + if (auto groupPtr = groupWeakPtr.lock(); groupPtr) { + auto &ggoRef = groupPtr->groupGraphicsObject(); + + if (usePastePos && !offsetInitialized) { + offset = pastePos - ggoRef.pos(); + offsetInitialized = true; + } + if (usePastePos) { + ggoRef.moveNodes(offset); + } + ggoRef.moveConnections(); + ggoRef.setSelected(true); + } + } + + for (QJsonValueRef node : nodesJsonArray) { + QJsonObject nodeObj = node.toObject(); + NodeId oldNodeId = static_cast(nodeObj.value("id").toInt(InvalidNodeId)); + + if (createdOldNodeIds.contains(oldNodeId)) { + continue; + } + + auto &nodeRef = loadNodeToMap(nodeObj, false); + + NodeId newNodeId = nodeRef.nodeId(); + QUuid oldId = encodeNodeId(oldNodeId); + QUuid newId = encodeNodeId(newNodeId); + IDMap.insert(std::make_pair(oldId, newId)); + + if (usePastePos && !offsetInitialized) { + offset = pastePos - nodeRef.pos(); + offsetInitialized = true; + } + if (usePastePos) { + nodeRef.moveBy(offset.x(), offset.y()); + } + nodeRef.moveConnections(); + nodeRef.setSelected(true); + } + + QJsonArray connectionJsonArray = jsonDocument.value("connections").toArray(); + for (QJsonValueRef connection : connectionJsonArray) { + auto nodeIdMap = convertMap(IDMap); + loadConnectionToMap(connection.toObject(), nodeIdMap); + + ConnectionId connId = fromJson(connection.toObject()); + auto it = _connectionGraphicsObjects.find(connId); + if (it != _connectionGraphicsObjects.end()) { + UniqueConnectionGraphicsObject &obj = it->second; + obj->setSelected(true); + } + } + + return IDMap; +} + +std::unordered_map BasicGraphicsScene::loadFromMemory(const QByteArray &data) +{ + std::unordered_map map = loadItems(data, QPointF(), false); + clearSelection(); + return map; +} + +QUuid BasicGraphicsScene::encodeNodeId(NodeId nodeId) +{ + QByteArray bytes(16, 0); + QDataStream stream(&bytes, QIODevice::WriteOnly); + stream << static_cast(nodeId); + return QUuid::fromRfc4122(bytes); +} + +NodeId BasicGraphicsScene::decodeNodeUuid(QUuid const &uuid) +{ + auto bytes = uuid.toRfc4122(); + if (bytes.size() < static_cast(sizeof(quint32))) + return QtNodes::InvalidNodeId; + + QDataStream stream(bytes); + quint32 value = 0; + stream >> value; + return static_cast(value); +} + +std::unordered_map BasicGraphicsScene::convertMap( + std::unordered_map const &uuidMap) +{ + std::unordered_map idMap; + + for (const auto &pair : uuidMap) { + NodeId keyNodeId = decodeNodeUuid(pair.first); + NodeId valueNodeId = decodeNodeUuid(pair.second); + + if (keyNodeId != QtNodes::InvalidNodeId && valueNodeId != QtNodes::InvalidNodeId) { + idMap[keyNodeId] = valueNodeId; + } + } + + return idMap; +} + std::weak_ptr BasicGraphicsScene::createGroupFromSelection(QString groupName) { if (!_groupingEnabled) @@ -636,25 +776,48 @@ void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, } std::pair, std::unordered_map> -BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) +BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson, + QHash const &nodeById) + { if (!_groupingEnabled) return {std::weak_ptr(), {}}; - // since the new nodes will have the same IDs as in the file and the connections - // need these old IDs to be restored, we must create new IDs and map them to the - // old ones so the connections are properly restored std::unordered_map IDsMap{}; std::unordered_map nodeIdMap{}; - std::vector group_children{}; - QJsonArray nodesJson = groupJson["nodes"].toArray(); - for (const QJsonValueRef nodeJson : nodesJson) { - QJsonObject nodeObject = nodeJson.toObject(); - NodeId const oldNodeId = jsonValueToNodeId(nodeObject["id"]); + QJsonArray nodesJson = groupJson.value("nodes").toArray(); + for (QJsonValue const &nodeVal : nodesJson) { + QJsonObject nodeObject; + + if (nodeVal.isDouble()) { + NodeId const oldNodeId = static_cast(nodeVal.toInt(InvalidNodeId)); + if (oldNodeId == InvalidNodeId) { + qWarning() << "restoreGroup(): invalid node id in group:" << nodeVal; + continue; + } + + auto it = nodeById.find(oldNodeId); + if (it == nodeById.end()) { + qWarning() << "restoreGroup(): group references missing node id:" << oldNodeId; + continue; + } + + nodeObject = it.value(); + } + + else if (nodeVal.isObject()) { + nodeObject = nodeVal.toObject(); + } else { + qWarning() << "restoreGroup(): unexpected node entry type:" << nodeVal; + continue; + } + + NodeId const oldNodeId = jsonValueToNodeId(nodeObject.value("id")); + + NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, /*keepOriginalId=*/false); - NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); NodeId const newNodeId = nodeRef.nodeId(); if (oldNodeId != InvalidNodeId) { @@ -665,12 +828,12 @@ BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) group_children.push_back(&nodeRef); } - QJsonArray connectionJsonArray = groupJson["connections"].toArray(); - for (auto connection : connectionJsonArray) { - loadConnectionToMap(connection.toObject(), nodeIdMap); + QJsonArray connectionJsonArray = groupJson.value("connections").toArray(); + for (QJsonValue const &connectionVal : connectionJsonArray) { + loadConnectionToMap(connectionVal.toObject(), nodeIdMap); } - return std::make_pair(createGroup(group_children, groupJson["name"].toString()), IDsMap); + return std::make_pair(createGroup(group_children, groupJson.value("name").toString()), IDsMap); } std::unordered_map> const &BasicGraphicsScene::groups() const @@ -802,17 +965,28 @@ std::weak_ptr BasicGraphicsScene::loadGroupFile() if (!file.open(QIODevice::ReadOnly)) { qDebug() << "Error loading group file!"; + return std::weak_ptr(); } QDir d = QFileInfo(fileName).absoluteDir(); - QString absolute = d.absolutePath(); - QDir::setCurrent(absolute); + QDir::setCurrent(d.absolutePath()); QByteArray wholeFile = file.readAll(); - const QJsonObject fileJson = QJsonDocument::fromJson(wholeFile).object(); - return restoreGroup(fileJson).first; + QHash nodeById; + + QJsonArray nodesArr = fileJson.value("nodes").toArray(); + nodeById.reserve(nodesArr.size()); + for (QJsonValue const &v : nodesArr) { + QJsonObject o = v.toObject(); + NodeId id = jsonValueToNodeId(o.value("id")); + if (id != InvalidNodeId) { + nodeById.insert(id, o); + } + } + + return restoreGroup(fileJson, nodeById).first; } GroupId BasicGraphicsScene::nextGroupId() diff --git a/test/src/TestNodeGroup.cpp b/test/src/TestNodeGroup.cpp index 6599656e..9742d9b3 100644 --- a/test/src/TestNodeGroup.cpp +++ b/test/src/TestNodeGroup.cpp @@ -343,7 +343,8 @@ TEST_CASE("Saving and restoring node groups", "[node-group]") BasicGraphicsScene newScene(newModel); newScene.setGroupingEnabled(true); - auto [restoredGroupWeak, idMapping] = newScene.restoreGroup(groupJson); + QHash nodeById; + auto [restoredGroupWeak, idMapping] = newScene.restoreGroup(groupJson, nodeById); auto restoredGroup = restoredGroupWeak.lock(); REQUIRE(restoredGroup);