mirror of
https://github.com/arabine/open-story-teller.git
synced 2025-12-06 17:09:06 +01:00
614 lines
15 KiB
C++
614 lines
15 KiB
C++
#include "story_graph_model.h"
|
|
#include "media_node_model.h"
|
|
|
|
#include <QtNodes/ConnectionIdUtils>
|
|
#include <QJsonArray>
|
|
#include <iterator>
|
|
#include <QDir>
|
|
|
|
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<NodeId> StoryGraphModel::allNodeIds() const
|
|
{
|
|
return _nodeIds;
|
|
}
|
|
|
|
std::unordered_set<ConnectionId> StoryGraphModel::allConnectionIds(NodeId const nodeId) const
|
|
{
|
|
std::unordered_set<ConnectionId> 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<ConnectionId> StoryGraphModel::connections(NodeId nodeId,
|
|
PortType portType,
|
|
PortIndex portIndex) const
|
|
{
|
|
std::unordered_set<ConnectionId> 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<StoryNodeBase> 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<QPointF>();
|
|
|
|
Q_EMIT nodePositionUpdated(nodeId);
|
|
|
|
result = true;
|
|
} break;
|
|
|
|
case NodeRole::Size: {
|
|
model->geometryData().size = value.value<QSize>();
|
|
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<qint64>(p.outNodeId)},
|
|
{"outPortIndex", static_cast<qint64>(p.outPortIndex)},
|
|
{"inNodeId", static_cast<qint64>(p.inNodeId)},
|
|
{"inPortIndex", static_cast<qint64>(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<QtNodes::ConnectionId>();
|
|
// Restore the connection
|
|
addConnection(connId);
|
|
}
|
|
}
|
|
|
|
nlohmann::json StoryGraphModel::SaveNode(NodeId const nodeId) const
|
|
{
|
|
nlohmann::json nodeJson;
|
|
nodeJson["id"] = static_cast<qint64>(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<QPointF>();
|
|
|
|
nlohmann::json posJson;
|
|
posJson["x"] = pos.x();
|
|
posJson["y"] = pos.y();
|
|
nodeJson["position"] = posJson;
|
|
|
|
nodeJson["inPortCount"] = nodeData(nodeId, NodeRole::InPortCount).value<int>();
|
|
nodeJson["outPortCount"] = nodeData(nodeId, NodeRole::OutPortCount).value<int>();
|
|
}
|
|
|
|
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<int>();
|
|
|
|
_nextNodeId = std::max(_nextNodeId, restoredNodeId + 1);
|
|
|
|
nlohmann::json internalDataJson = nodeJson["internal-data"];
|
|
|
|
std::string delegateModelName = internalDataJson["model-name"].get<std::string>();
|
|
|
|
// std::unique_ptr<NodeDelegateModel> 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<double>(), posJson["y"].get<double>());
|
|
|
|
setNodeData(restoredNodeId, NodeRole::Position, pos);
|
|
model->SetOutPortCount(nodeJson["outPortCount"].get<int>());
|
|
|
|
_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();
|
|
}
|