#include "story_graph_model.h" #include "media_node_model.h" #include #include #include #include extern "C" int miniaudio_play(const char* filename); StoryGraphModel::StoryGraphModel(StoryProject &project) : m_project(project) { m_audioThread = std::thread( std::bind(&StoryGraphModel::AudioThread, this) ); // connect(m_player, &QMediaPlayer::playbackStateChanged, this, [&](QMediaPlayer::PlaybackState newState) { // if (newState == QMediaPlayer::PlaybackState::StoppedState) { // m_player->stop(); // emit sigAudioStopped(); // } // }); } StoryGraphModel::~StoryGraphModel() { // Quit audio thread m_audioQueue.push({"quit", ""}); if (m_audioThread.joinable()) { m_audioThread.join(); } } void StoryGraphModel::AudioThread() { for (;;) { auto cmd = m_audioQueue.front(); if (cmd.order == "play") { miniaudio_play(cmd.filename.c_str()); QMetaObject::invokeMethod(this, "sigAudioStopped", Qt::QueuedConnection); m_audioQueue.pop(); } else { return; } } } void StoryGraphModel::PlaySound(const QString &fileName) { m_audioQueue.push({"play", fileName.toStdString()}); } std::unordered_set StoryGraphModel::allNodeIds() const { return _nodeIds; } std::unordered_set StoryGraphModel::allConnectionIds(NodeId const nodeId) const { std::unordered_set result; std::copy_if(_connectivity.begin(), _connectivity.end(), std::inserter(result, std::end(result)), [&nodeId](ConnectionId const &cid) { return cid.inNodeId == nodeId || cid.outNodeId == nodeId; }); return result; } std::unordered_set StoryGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { std::unordered_set result; std::copy_if(_connectivity.begin(), _connectivity.end(), std::inserter(result, std::end(result)), [&portType, &portIndex, &nodeId](ConnectionId const &cid) { return (getNodeId(portType, cid) == nodeId && getPortIndex(portType, cid) == portIndex); }); return result; } bool StoryGraphModel::connectionExists(ConnectionId const connectionId) const { return (_connectivity.find(connectionId) != _connectivity.end()); } NodeId StoryGraphModel::addNode(QString const nodeType) { NodeId id = InvalidNodeId; std::cout << "-------------------> add node: " << nodeType.toStdString() << std::endl; try { NodeId newId = newNodeId(); // Create new node. _nodeIds.insert(newId); _models[newId] = createNode(nodeType.toStdString()); _models[newId]->setNodeId(newId); Q_EMIT nodeCreated(newId); id = newId; } catch(std::invalid_argument e) { std::cout << e.what() << std::endl; } return id; } NodeId StoryGraphModel::addNode(std::shared_ptr model) { NodeId newId = newNodeId(); // Create new node. _nodeIds.insert(newId); _models[newId] = model; Q_EMIT nodeCreated(newId); return newId; } bool StoryGraphModel::connectionPossible(ConnectionId const connectionId) const { return !connectionExists(connectionId); } void StoryGraphModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); Q_EMIT connectionCreated(connectionId); } bool StoryGraphModel::nodeExists(NodeId const nodeId) const { return (_nodeIds.find(nodeId) != _nodeIds.end()); } QVariant StoryGraphModel::nodeData(NodeId nodeId, NodeRole role) const { QVariant result; auto it = _models.find(nodeId); if (it == _models.end()) return result; auto &model = it->second; switch (role) { case NodeRole::Type: result = QString("Default Node Type"); break; case NodeRole::Position: result = model->geometryData().pos; break; case NodeRole::Size: result = model->geometryData().size; break; case NodeRole::CaptionVisible: result = true; break; case NodeRole::Caption: result = model->caption(); break; case NodeRole::Style: { auto style = StyleCollection::nodeStyle(); result = style.toJson().toVariantMap(); } break; case NodeRole::InternalData: break; case NodeRole::InPortCount: result = model->nPorts(PortType::In); break; case NodeRole::OutPortCount: result = model->nPorts(PortType::Out); break; case NodeRole::Widget: { auto w = model->embeddedWidget(); result = QVariant::fromValue(w); break; } case NodeRole::Id: result = model->getNodeId(); break; } return result; } void StoryGraphModel::SetInternalData(NodeId nodeId, nlohmann::json &j) { auto it = _models.find(nodeId); if (it == _models.end()) return; auto &model = it->second; model->setInternalData(j); } bool StoryGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant value) { bool result = false; auto it = _models.find(nodeId); if (it == _models.end()) return result; auto &model = it->second; switch (role) { case NodeRole::Id: break; case NodeRole::Type: break; case NodeRole::Position: { model->geometryData().pos = value.value(); Q_EMIT nodePositionUpdated(nodeId); result = true; } break; case NodeRole::Size: { model->geometryData().size = value.value(); result = true; } break; case NodeRole::CaptionVisible: break; case NodeRole::Caption: break; case NodeRole::Style: break; case NodeRole::InternalData: break; case NodeRole::InPortCount: break; case NodeRole::OutPortCount: break; case NodeRole::Widget: break; } return result; } QVariant StoryGraphModel::portData(NodeId nodeId, PortType portType, PortIndex portIndex, PortRole role) const { switch (role) { case PortRole::Data: return QVariant(); break; case PortRole::DataType: return QVariant(); break; case PortRole::ConnectionPolicyRole: return QVariant::fromValue(ConnectionPolicy::One); break; case PortRole::CaptionVisible: return true; break; case PortRole::Caption: if (portType == PortType::In) return QString::fromUtf8("Port In"); else return QString::fromUtf8("Port Out"); break; } return QVariant(); } bool StoryGraphModel::setPortData( NodeId nodeId, PortType portType, PortIndex portIndex, QVariant const &value, PortRole role) { Q_UNUSED(nodeId); Q_UNUSED(portType); Q_UNUSED(portIndex); Q_UNUSED(value); Q_UNUSED(role); return false; } bool StoryGraphModel::deleteConnection(ConnectionId const connectionId) { bool disconnected = false; auto it = _connectivity.find(connectionId); if (it != _connectivity.end()) { disconnected = true; _connectivity.erase(it); }; if (disconnected) Q_EMIT connectionDeleted(connectionId); return disconnected; } bool StoryGraphModel::deleteNode(NodeId const nodeId) { // Delete connections to this node first. auto connectionIds = allConnectionIds(nodeId); for (auto &cId : connectionIds) { deleteConnection(cId); } _nodeIds.erase(nodeId); Q_EMIT nodeDeleted(nodeId); return true; } namespace QtNodes { void to_json(nlohmann::json& j, const ConnectionId& p) { j = nlohmann::json{ {"outNodeId", static_cast(p.outNodeId)}, {"outPortIndex", static_cast(p.outPortIndex)}, {"inNodeId", static_cast(p.inNodeId)}, {"inPortIndex", static_cast(p.inPortIndex)}, }; } void from_json(const nlohmann::json& j, ConnectionId& p) { j.at("outNodeId").get_to(p.outNodeId); j.at("outPortIndex").get_to(p.outPortIndex); j.at("inNodeId").get_to(p.inNodeId); j.at("inPortIndex").get_to(p.inPortIndex); } } // namespace QtNodes nlohmann::json StoryGraphModel::Save() const { nlohmann::json j; nlohmann::json nodesJsonArray; for (auto const nodeId : allNodeIds()) { nodesJsonArray.push_back(SaveNode(nodeId)); } j["nodes"] = nodesJsonArray; nlohmann::json connJsonArray; for (auto const &cid : _connectivity) { nlohmann::json o = cid; connJsonArray.push_back(o); } j["connections"] = connJsonArray; return j; } void StoryGraphModel::Load(const nlohmann::json &j) { nlohmann::json nodesJsonArray = j["nodes"]; for (auto& element : nodesJsonArray) { LoadNode(element); } std::cout << j.dump(4) << std::endl; nlohmann::json connectionJsonArray = j["connections"]; for (auto& connection : connectionJsonArray) { ConnectionId connId = connection.get(); // Restore the connection addConnection(connId); } } nlohmann::json StoryGraphModel::SaveNode(NodeId const nodeId) const { nlohmann::json nodeJson; nodeJson["id"] = static_cast(nodeId); auto it = _models.find(nodeId); if (it == _models.end()) return nodeJson; auto &model = it->second; nodeJson["internal-data"] = model->ToJson(); { QPointF const pos = nodeData(nodeId, NodeRole::Position).value(); nlohmann::json posJson; posJson["x"] = pos.x(); posJson["y"] = pos.y(); nodeJson["position"] = posJson; nodeJson["inPortCount"] = nodeData(nodeId, NodeRole::InPortCount).value(); nodeJson["outPortCount"] = nodeData(nodeId, NodeRole::OutPortCount).value(); } return nodeJson; } void StoryGraphModel::LoadNode(const nlohmann::json &nodeJson) { // Possibility of the id clash when reading it from json and not generating a // new value. // 1. When restoring a scene from a file. // Conflict is not possible because the scene must be cleared by the time of // loading. // 2. When undoing the deletion command. Conflict is not possible // because all the new ids were created past the removed nodes. NodeId restoredNodeId = nodeJson["id"].get(); _nextNodeId = std::max(_nextNodeId, restoredNodeId + 1); nlohmann::json internalDataJson = nodeJson["internal-data"]; std::string delegateModelName = internalDataJson["model-name"].get(); // std::unique_ptr model = _registry->create(delegateModelName); auto model = createNode(delegateModelName); if (model) { // connect(model.get(), // &NodeDelegateModel::dataUpdated, // [restoredNodeId, this](PortIndex const portIndex) { // onOutPortDataUpdated(restoredNodeId, portIndex); // }); _models[restoredNodeId] = model; model->setNodeId(restoredNodeId); _nodeIds.insert(restoredNodeId); Q_EMIT nodeCreated(restoredNodeId); nlohmann::json posJson = nodeJson["position"]; QPointF const pos(posJson["x"].get(), posJson["y"].get()); setNodeData(restoredNodeId, NodeRole::Position, pos); model->SetOutPortCount(nodeJson["outPortCount"].get()); _models[restoredNodeId]->FromJson(internalDataJson); } else { throw std::logic_error(std::string("No registered model with name ") + delegateModelName); } } NodeId StoryGraphModel::FindFirstNode() const { NodeId id{InvalidNodeId}; // First node is the one without connection on its input port for (auto const nodeId : allNodeIds()) { bool foundConnection{false}; for (auto& c : _connectivity) { if (c.inNodeId == nodeId) { foundConnection = true; } } if (!foundConnection) { id = nodeId; qDebug() << "First node is: " << id; break; } } return id; } std::string StoryGraphModel::Build() { std::stringstream chip32; NodeId firstNode = FindFirstNode(); chip32 << "\tjump " << GetNodeEntryLabel(firstNode) << "\r\n"; // First generate all constants for (auto const nodeId : allNodeIds()) { auto it = _models.find(nodeId); if (it != _models.end()) { chip32 << it->second->GenerateConstants() << "\n"; } } nlohmann::json nodesJsonArray; for (auto const nodeId : allNodeIds()) { auto it = _models.find(nodeId); if (it != _models.end()) { chip32 << it->second->Build() << "\n"; } } return chip32.str(); } std::string StoryGraphModel::GetNodeEntryLabel(NodeId nodeId) const { std::string label; auto it = _models.find(nodeId); if (it != _models.end()) { label = it->second->EntryLabel(); } return label; } void StoryGraphModel::addPort(NodeId nodeId, PortType portType, PortIndex portIndex) { // STAGE 1. // Compute new addresses for the existing connections that are shifted and // placed after the new ones PortIndex first = portIndex; PortIndex last = first; portsAboutToBeInserted(nodeId, portType, first, last); // STAGE 3. Re-create previouly existed and now shifted connections portsInserted(); } void StoryGraphModel::removePort(NodeId nodeId, PortType portType, PortIndex portIndex) { // STAGE 1. // Compute new addresses for the existing connections that are shifted upwards // instead of the deleted ports. PortIndex first = portIndex; PortIndex last = first; portsAboutToBeDeleted(nodeId, portType, first, last); portsDeleted(); } QString StoryGraphModel::GetImagesDir() const { return QString(m_project.GetWorkingDir().c_str()) + QDir::separator() + "images"; } QString StoryGraphModel::BuildFullImagePath(const QString &fileName) const { return GetImagesDir() + QDir::separator() + fileName; } QString StoryGraphModel::GetSoundsDir() const { return QString(m_project.GetWorkingDir().c_str()) + QDir::separator() + "sounds"; } QString StoryGraphModel::BuildFullSoundPath(const QString &fileName) const { return GetSoundsDir() + QDir::separator() + fileName; } void StoryGraphModel::Clear() { _nodeIds.clear(); _connectivity.clear(); _models.clear(); emit modelReset(); }