Big overall architecture change: separation between display and story model

This commit is contained in:
Anthony Rabine 2024-05-01 00:09:15 +02:00
parent 4fc34d5521
commit 73aefa7100
28 changed files with 904 additions and 703 deletions

16
shared/i_story_project.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <list>
#include <string>
#include "connection.h"
class IStoryProject
{
public:
virtual ~IStoryProject() {};
virtual std::list<std::shared_ptr<Connection>> GetNodeConnections(const std::string &nodeId) = 0;
virtual int OutputsCount(const std::string &nodeId) = 0;
};

View file

@ -131,7 +131,14 @@ void LibraryManager::Save()
void LibraryManager::CopyToDevice(const std::string &outputDir) void LibraryManager::CopyToDevice(const std::string &outputDir)
{ {
std::thread myThread([&]() { std::thread myThread([&]() {
myThread.detach(); myThread.detach();
std::cout << "Starting to copy elements" << std::endl;
for (auto p : *this)
{
}
}); });
} }

View file

@ -77,7 +77,7 @@ private:
class IResource class IResource
{ {
public: public:
virtual ~IResource(); virtual ~IResource() {}
}; };

View file

@ -6,10 +6,11 @@
#include <regex> #include <regex>
#include "json.hpp" #include "json.hpp"
#include "media_node.h"
StoryProject::StoryProject() StoryProject::StoryProject()
{ {
registerNode<MediaNode>("media-node");
} }
StoryProject::~StoryProject() StoryProject::~StoryProject()
@ -27,6 +28,13 @@ void StoryProject::SetPaths(const std::string &uuid, const std::string &library_
std::cout << "Working dir is: " << m_working_dir << std::endl; std::cout << "Working dir is: " << m_working_dir << std::endl;
} }
void StoryProject::CopyToDevice(const std::string &outputDir)
{
ResourceManager manager;
Load(manager);
}
void StoryProject::New(const std::string &uuid, const std::string &library_path) void StoryProject::New(const std::string &uuid, const std::string &library_path)
{ {
@ -70,8 +78,217 @@ bool StoryProject::ParseStoryInformation(nlohmann::json &j)
return success; return success;
} }
void StoryProject::ModelToJson(nlohmann::json &model)
{
nlohmann::json nodes = nlohmann::json::array();
for (const auto & n : m_nodes)
{
nodes.push_back(n->ToJson());
}
bool StoryProject::Load(nlohmann::json &model, ResourceManager &manager) model["nodes"] = nodes;
// Save links
nlohmann::json connections = nlohmann::json::array();
for (const auto& cnx : m_links)
{
nlohmann::json c(*cnx);
connections.push_back(c);
// Connection cnx = LinkToModel(linkInfo->ed_link->InputId, linkInfo->ed_link->OutputId);
}
model["connections"] = connections;
}
std::shared_ptr<BaseNode> StoryProject::CreateNode(const std::string &type)
{
typename Registry::const_iterator i = m_registry.find(type);
if (i == m_registry.end()) {
throw std::invalid_argument(std::string(__PRETTY_FUNCTION__) +
": key not registered");
}
else
{
return i->second(type);
}
}
void StoryProject::AddConnection(std::shared_ptr<Connection> c)
{
m_links.push_back(c);
}
void StoryProject::DeleteNode(const std::string &id)
{
auto it = std::find_if(m_nodes.begin(),
m_nodes.end(),
[&id](std::shared_ptr<BaseNode> const &n) { return n->GetId() == id; });
if ( it != m_nodes.end() )
{
it->reset();
m_nodes.erase(it);
}
}
void StoryProject::DeleteLink(std::shared_ptr<Connection> c)
{
auto it = std::find_if(m_links.begin(),
m_links.end(),
[&c](std::shared_ptr<Connection> const &cnx) {
return *cnx == *c;
});
if ( it != m_links.end() )
{
it->reset();
m_links.erase(it);
}
}
bool StoryProject::ModelFromJson(const nlohmann::json &model)
{
bool success = false;
try {
nlohmann::json nodesJsonArray = model["nodes"];
m_nodes.clear();
m_links.clear();
for (auto& element : nodesJsonArray) {
std::string type = element["type"].get<std::string>();
auto n = CreateNode(type);
if (n)
{
n->FromJson(element);
m_nodes.push_back(n);
}
else
{
throw std::logic_error(std::string("No registered model with name ") + type);
}
}
// std::cout << model.dump(4) << std::endl;
// Ici on reste flexible sur les connexions, cela permet de créer éventuellement des
// projets sans fils (bon, l'élément devrait quand même exister dans le JSON)
if (model.contains("connections"))
{
nlohmann::json connectionJsonArray = model["connections"];
// key: node UUID, value: output counts
std::map<std::string, int> outputCounts;
for (auto& connection : connectionJsonArray)
{
m_links.push_back(std::make_shared<Connection>(connection.get<Connection>()));
}
}
success = true;
}
catch(std::exception &e)
{
std::cout << "(NodeEditorWindow::Load) " << e.what() << std::endl;
}
return success;
}
int StoryProject::OutputsCount(const std::string &nodeId)
{
int count = 0;
for (const auto & l : m_links)
{
if (l->outNodeId == nodeId)
{
count++;
}
}
return count;
}
std::list<std::shared_ptr<Connection>> StoryProject::GetNodeConnections(const std::string &nodeId)
{
std::list<std::shared_ptr<Connection>> c;
for (const auto & l : m_links)
{
if (l->outNodeId == nodeId)
{
c.push_back(l);
}
}
return c;
}
std::string StoryProject::FindFirstNode() const
{
std::string id;
// First node is the one without connection on its input port
for (const auto & n : m_nodes)
{
bool foundConnection = false;
for (const auto& l : m_links)
{
if (l->inNodeId == n->GetId())
{
foundConnection = true;
}
}
if (!foundConnection)
{
id = n->GetId();
std::cout << "First node is: " + id << std::endl;
break;
}
}
return id;
}
bool StoryProject::Build(std::string &codeStr)
{
std::stringstream code;
std::stringstream chip32;
std::string firstNode = FindFirstNode();
if (firstNode == "")
{
std::cout << "First node not found, there must be only one node with a free input." << std::endl;
return false;
}
code << "\tjump " << BaseNode::GetEntryLabel(firstNode) << "\r\n";
// First generate all constants
for (const auto & n : m_nodes)
{
code << n->GenerateConstants(*this, OutputsCount(n->GetId())) << "\n";
}
for (const auto & n : m_nodes)
{
code << n->Build(*this, OutputsCount(n->GetId())) << "\n";
}
codeStr = code.str();
return true;
}
bool StoryProject::Load(ResourceManager &manager)
{ {
try { try {
std::ifstream f(m_project_file_path); std::ifstream f(m_project_file_path);
@ -98,7 +315,7 @@ bool StoryProject::Load(nlohmann::json &model, ResourceManager &manager)
if (j.contains("nodegraph")) if (j.contains("nodegraph"))
{ {
model = j["nodegraph"]; ModelFromJson(j["nodegraph"]);
m_initialized = true; m_initialized = true;
} }
} }
@ -173,7 +390,7 @@ bool StoryProject::Load(nlohmann::json &model, ResourceManager &manager)
return m_initialized; return m_initialized;
} }
void StoryProject::Save(const nlohmann::json &model, ResourceManager &manager) void StoryProject::Save(ResourceManager &manager)
{ {
nlohmann::json j; nlohmann::json j;
j["project"] = { {"name", m_name}, {"uuid", m_uuid}, { "title_image", m_titleImage }, { "title_sound", m_titleSound } }; j["project"] = { {"name", m_name}, {"uuid", m_uuid}, { "title_image", m_titleImage }, { "title_sound", m_titleSound } };
@ -194,6 +411,8 @@ void StoryProject::Save(const nlohmann::json &model, ResourceManager &manager)
j["resources"] = resourcesData; j["resources"] = resourcesData;
} }
nlohmann::json model;
ModelToJson(model);
j["nodegraph"] = model; j["nodegraph"] = model;
std::ofstream o(m_project_file_path); std::ofstream o(m_project_file_path);
@ -240,6 +459,8 @@ void StoryProject::Clear()
m_working_dir = ""; m_working_dir = "";
m_project_file_path = ""; m_project_file_path = "";
m_initialized = false; m_initialized = false;
m_nodes.clear();
m_links.clear();
} }
void StoryProject::EraseString(std::string &theString, const std::string &toErase) void StoryProject::EraseString(std::string &theString, const std::string &toErase)

View file

@ -8,7 +8,9 @@
#include "json.hpp" #include "json.hpp"
#include "resource_manager.h" #include "resource_manager.h"
#include "connection.h"
#include "base_node.h"
#include "i_story_project.h"
// FIXME : Structure très Lunii style, à utiliser pour la conversion peut-être ... // FIXME : Structure très Lunii style, à utiliser pour la conversion peut-être ...
struct StoryNode struct StoryNode
@ -42,7 +44,7 @@ struct StoryNode
}; };
struct StoryProject struct StoryProject : public IStoryProject
{ {
public: public:
@ -51,6 +53,10 @@ public:
StoryProject(); StoryProject();
~StoryProject(); ~StoryProject();
bool *Selected() {
return &m_selected;
}
/* /*
std::vector<StoryNode> m_nodes; std::vector<StoryNode> m_nodes;
@ -60,14 +66,28 @@ public:
StoryNode *m_tree; StoryNode *m_tree;
*/ */
void New(const std::string &uuid, const std::string &library_path); void New(const std::string &uuid, const std::string &library_path);
bool Load(nlohmann::json &model, ResourceManager &manager); bool Build(std::string &codeStr);
void Save(const nlohmann::json &model, ResourceManager &manager); bool Load(ResourceManager &manager);
void Save(ResourceManager &manager);
void SaveBinary(const std::vector<uint8_t> &m_program); void SaveBinary(const std::vector<uint8_t> &m_program);
void SetPaths(const std::string &uuid, const std::string &library_path); void SetPaths(const std::string &uuid, const std::string &library_path);
void CopyToDevice(const std::string &outputDir);
void ModelToJson(nlohmann::json &model);
bool ModelFromJson(const nlohmann::json &model);
void CreateTree(); void CreateTree();
void Clear(); void Clear();
std::pair<std::list<std::shared_ptr<BaseNode>>::iterator, std::list<std::shared_ptr<BaseNode>>::iterator> Nodes() {
return std::make_pair(m_nodes.begin(), m_nodes.end());
}
std::pair<std::list<std::shared_ptr<Connection>>::iterator, std::list<std::shared_ptr<Connection>>::iterator> Links() {
return std::make_pair(m_links.begin(), m_links.end());
}
void Select(bool selected) { m_selected = selected; } void Select(bool selected) { m_selected = selected; }
bool IsSelected() const { return m_selected; } bool IsSelected() const { return m_selected; }
@ -104,11 +124,21 @@ public:
// Initialize with an existing project // Initialize with an existing project
const bool IsInitialized() const { return m_initialized; } const bool IsInitialized() const { return m_initialized; }
static void EraseString(std::string &theString, const std::string &toErase); static void EraseString(std::string &theString, const std::string &toErase);
static std::string ToUpper(const std::string &input); static std::string ToUpper(const std::string &input);
bool ParseStoryInformation(nlohmann::json &j); bool ParseStoryInformation(nlohmann::json &j);
// From IStoryProject
virtual std::list<std::shared_ptr<Connection>> GetNodeConnections(const std::string &nodeId) override;
std::string FindFirstNode() const;
virtual int OutputsCount(const std::string &nodeId) override;
std::shared_ptr<BaseNode> CreateNode(const std::string& type);
void AddConnection(std::shared_ptr<Connection> c);
void DeleteNode(const std::string &id);
void DeleteLink(std::shared_ptr<Connection> c);
private: private:
// Project properties and location // Project properties and location
std::string m_name; /// human readable name std::string m_name; /// human readable name
@ -121,6 +151,10 @@ private:
std::filesystem::path m_assetsPath; std::filesystem::path m_assetsPath;
// Model in memory
std::list<std::shared_ptr<Connection>> m_links;
std::list<std::shared_ptr<BaseNode>> m_nodes;
bool m_initialized{false}; bool m_initialized{false};
std::filesystem::path m_working_dir; /// Temporary folder based on the uuid, where the archive is unzipped std::filesystem::path m_working_dir; /// Temporary folder based on the uuid, where the archive is unzipped
@ -131,6 +165,28 @@ private:
ImageFormat m_imageFormat{IMG_FORMAT_BMP_4BITS}; ImageFormat m_imageFormat{IMG_FORMAT_BMP_4BITS};
SoundFormat m_soundFormat{SND_FORMAT_WAV}; SoundFormat m_soundFormat{SND_FORMAT_WAV};
template<class NodeType>
struct Factory {
static std::shared_ptr<BaseNode> create_func(const std::string &type) {
return std::make_shared<NodeType>(type);
}
};
typedef std::shared_ptr<BaseNode> (*GenericCreator)(const std::string &type);
typedef std::map<std::string, GenericCreator> Registry;
Registry m_registry;
template<class Derived>
void registerNode(const std::string& key) {
m_registry.insert(typename Registry::value_type(key, Factory<Derived>::create_func));
}
}; };
#endif // STORY_PROJECT_H #endif // STORY_PROJECT_H

View file

@ -171,12 +171,13 @@ set(SRCS
src/node_engine/base_node.h src/node_engine/base_node.h
src/node_engine/base_node.cpp src/node_engine/base_node.cpp
src/node_engine/media_node.h
src/node_engine/media_node.cpp
src/node_engine/connection.cpp src/node_engine/connection.cpp
src/node_engine/connection.h src/node_engine/connection.h
src/node_editor/media_node_widget.h
src/node_editor/media_node.h src/node_editor/media_node_widget.cpp
src/node_editor/media_node.cpp
src/node_editor/base_node_widget.h src/node_editor/base_node_widget.h
src/node_editor/base_node_widget.cpp src/node_editor/base_node_widget.cpp
src/node_editor/node_editor_window.h src/node_editor/node_editor_window.h

View file

@ -109,6 +109,6 @@ void EmulatorWindow::ClearImage()
void EmulatorWindow::SetImage(const std::string &image) void EmulatorWindow::SetImage(const std::string &image)
{ {
m_image.name = image; m_imageFileName = image;
m_image.Load(m_story.BuildFullAssetsPath(image)); m_image.Load(m_story.BuildFullAssetsPath(image));
} }

View file

@ -18,5 +18,6 @@ public:
private: private:
IStoryManager &m_story; IStoryManager &m_story;
Gui::Image m_image; Gui::Image m_image;
std::string m_imageFileName;
}; };

View file

@ -18,8 +18,6 @@ public:
int w; int w;
int h; int h;
std::string name;
bool Valid() const { bool Valid() const {
return (w && h); return (w && h);
} }

View file

@ -9,6 +9,7 @@
#include "resource.h" #include "resource.h"
#include "connection.h" #include "connection.h"
#include "base_node.h"
template <typename T> template <typename T>
struct Callback; struct Callback;
@ -46,8 +47,11 @@ public:
// Node interaction // Node interaction
virtual void Build(bool compileonly) = 0; virtual void Build(bool compileonly) = 0;
virtual std::shared_ptr<BaseNode> CreateNode(const std::string &type) = 0;
virtual void DeleteNode(const std::string &id) = 0;
virtual void DeleteLink(std::shared_ptr<Connection> c) = 0;
virtual std::list<std::shared_ptr<Connection>> GetNodeConnections(const std::string &nodeId) = 0; virtual std::list<std::shared_ptr<Connection>> GetNodeConnections(const std::string &nodeId) = 0;
virtual std::string GetNodeEntryLabel(const std::string &nodeId) = 0;
virtual void Play() = 0; virtual void Play() = 0;
virtual void Ok() = 0; virtual void Ok() = 0;
virtual void Stop() = 0; virtual void Stop() = 0;

View file

@ -429,7 +429,6 @@ bool PackArchive::ImportStudioFormat(const std::string &fileName, const std::str
nlohmann::json j = nlohmann::json::parse(f); nlohmann::json j = nlohmann::json::parse(f);
StoryProject proj; StoryProject proj;
ResourceManager res; ResourceManager res;
nlohmann::json model;
if (j.contains("title")) if (j.contains("title"))
{ {
@ -458,58 +457,53 @@ bool PackArchive::ImportStudioFormat(const std::string &fileName, const std::str
// Key: actionNode, value: Stage UUID // Key: actionNode, value: Stage UUID
std::map<std::string, std::string> stageActionLink; std::map<std::string, std::string> stageActionLink;
nlohmann::json jnodes = nlohmann::json::array();
for (const auto & n : j["stageNodes"]) for (const auto & n : j["stageNodes"])
{ {
nlohmann::json node; auto node = proj.CreateNode("media-node");
auto node_uuid = n["uuid"].get<std::string>(); if (node)
node["uuid"] = node_uuid; {
node["type"] = "media-node"; auto node_uuid = n["uuid"].get<std::string>();
node["position"] = n["position"]; node->SetId(node_uuid);
node->SetPosition(n["position"]["x"].get<float>(), n["position"]["y"].get<float>());
nlohmann::json internalData;
auto img = n["image"];
internalData["image"] = img.is_string() ? img.get<std::string>() : "";
auto audio = n["audio"];
internalData["sound"] = audio.is_string() ? audio.get<std::string>() : "";
nlohmann::json internalData; node->SetInternalData(internalData);
auto img = n["image"];
internalData["image"] = img.is_string() ? img.get<std::string>() : "";
auto audio = n["audio"];
internalData["sound"] = audio.is_string() ? audio.get<std::string>() : "";
node["internal-data"] = internalData; stageActionLink[n["okTransition"]["actionNode"]] = node_uuid;
}
stageActionLink[n["okTransition"]["actionNode"]] = node_uuid;
/* /*
"okTransition":{ "okTransition":{
"actionNode":"19d7328f-d0d2-4443-a7a2-25270dafe52c", "actionNode":"19d7328f-d0d2-4443-a7a2-25270dafe52c",
"optionIndex":0 "optionIndex":0
}, },
*/ */
jnodes.push_back(node);
} }
model["nodes"] = jnodes;
nlohmann::json connections = nlohmann::json::array();
for (const auto & n : j["actionNodes"]) for (const auto & n : j["actionNodes"])
{ {
std::string action_node_uuid = n["id"].get<std::string>(); // le champs est "id" et non pas "uuid", pénible std::string action_node_uuid = n["id"].get<std::string>(); // le champs est "id" et non pas "uuid", pénible
if (stageActionLink.count(action_node_uuid) > 0) if (stageActionLink.count(action_node_uuid) > 0)
{ {
int i = 0; int i = 0;
for (const auto & m : n["options"]) for (const auto & m : n["options"])
{ {
nlohmann::json c; auto c = std::make_shared<Connection>();
c["outNodeId"] = stageActionLink[action_node_uuid]; c->outNodeId = stageActionLink[action_node_uuid];
c["outPortIndex"] = i; c->outPortIndex = i;
c["inNodeId"] = m; // On prend le stage node; c->inNodeId = m; // On prend le stage node;
c["inPortIndex"] = 0; c->inPortIndex = 0;
i++; i++;
connections.push_back(c); proj.AddConnection(c);
} }
} }
else else
@ -517,11 +511,8 @@ bool PackArchive::ImportStudioFormat(const std::string &fileName, const std::str
std::cout << "ActionNode UUID not found" << std::endl; std::cout << "ActionNode UUID not found" << std::endl;
} }
} }
model["connections"] = connections;
// Save on disk // Save on disk
proj.Save(model, res); proj.Save(res);
} }
} }
catch(std::exception &e) catch(std::exception &e)

View file

@ -368,16 +368,6 @@ void LibraryWindow::Draw()
WindowBase::BeginDraw(); WindowBase::BeginDraw();
ImGui::SetWindowSize(ImVec2(626, 744), ImGuiCond_FirstUseEver); ImGui::SetWindowSize(ImVec2(626, 744), ImGuiCond_FirstUseEver);
if (ImGui::Button( ICON_MDI_FOLDER " Select directory"))
{
IGFD::FileDialogConfig config;
config.path = ".";
config.countSelectionMax = 1;
config.flags = ImGuiFileDialogFlags_Modal;
ImGuiFileDialog::Instance()->OpenDialog("ChooseLibraryDirDialog", "Choose a library directory", nullptr, config);
}
if (ImGui::Button(ICON_MDI_FOLDER " Export to device")) if (ImGui::Button(ICON_MDI_FOLDER " Export to device"))
{ {
IGFD::FileDialogConfig config; IGFD::FileDialogConfig config;
@ -388,6 +378,16 @@ void LibraryWindow::Draw()
ImGuiFileDialog::Instance()->OpenDialog("ChooseOutputDirDialog", "Choose an output directory", nullptr, config); ImGuiFileDialog::Instance()->OpenDialog("ChooseOutputDirDialog", "Choose an output directory", nullptr, config);
} }
if (ImGui::Button( ICON_MDI_FOLDER " Select directory"))
{
IGFD::FileDialogConfig config;
config.path = ".";
config.countSelectionMax = 1;
config.flags = ImGuiFileDialogFlags_Modal;
ImGuiFileDialog::Instance()->OpenDialog("ChooseLibraryDirDialog", "Choose a library directory", nullptr, config);
}
if (!m_libraryManager.IsInitialized()) if (!m_libraryManager.IsInitialized())
{ {
ImGui::SameLine(); ImGui::SameLine();
@ -470,8 +470,7 @@ void LibraryWindow::Draw()
ImGui::TableSetColumnIndex(2); ImGui::TableSetColumnIndex(2);
ImGui::TableHeader(ImGui::TableGetColumnName(2)); ImGui::TableHeader(ImGui::TableGetColumnName(2));
int internal_id = 1; int internal_id = 1;
uint32_t row = 0;
for (auto &p : m_libraryManager) for (auto &p : m_libraryManager)
{ {
ImGui::TableNextColumn(); ImGui::TableNextColumn();
@ -480,19 +479,15 @@ void LibraryWindow::Draw()
ImGui::PushID(internal_id++); ImGui::PushID(internal_id++);
// Select // Select
ImGui::TableNextColumn(); ImGui::TableNextColumn();
bool state = p->IsSelected(); ImGui::Checkbox("", p->Selected());
ImGui::Checkbox("", &state);
if (!state && select_all) if (!p->IsSelected() && select_all)
{ {
select_all = false; // Désélectionner "Select All" si un des items est désélectionné select_all = false; // Désélectionner "Select All" si un des items est désélectionné
} }
row++;
ImGui::TableNextColumn(); ImGui::TableNextColumn();
if (ImGui::SmallButton("Load")) if (ImGui::SmallButton("Load"))
{ {
m_storyManager.OpenProject(p->GetUuid()); m_storyManager.OpenProject(p->GetUuid());

View file

@ -446,6 +446,8 @@ void MainWindow::NewProjectPopup()
if (!std::filesystem::is_directory(projdir)) if (!std::filesystem::is_directory(projdir))
{ {
CloseProject();
m_story = m_libraryManager.NewProject(); m_story = m_libraryManager.NewProject();
if (m_story) if (m_story)
@ -608,25 +610,22 @@ void MainWindow::ProjectPropertiesPopup()
void MainWindow::SaveProject() void MainWindow::SaveProject()
{ {
nlohmann::json model; nlohmann::json model;
m_nodeEditorWindow.Save(model); m_story->Save(m_resources);
m_story->Save(model, m_resources);
} }
void MainWindow::OpenProject(const std::string &uuid) void MainWindow::OpenProject(const std::string &uuid)
{ {
CloseProject(); CloseProject();
nlohmann::json model;
m_story = m_libraryManager.GetStory(uuid); m_story = m_libraryManager.GetStory(uuid);
if (!m_story) if (!m_story)
{ {
Log("Cannot find story: " + uuid); Log("Cannot find story: " + uuid);
} }
else if (m_story->Load(model, m_resources)) else if (m_story->Load(m_resources))
{ {
Log("Open project success"); Log("Open project success");
m_nodeEditorWindow.Load(model); m_nodeEditorWindow.Load(m_story);
auto proj = m_story->GetProjectFilePath(); auto proj = m_story->GetProjectFilePath();
// Add to recent if not exists // Add to recent if not exists
if (std::find(m_recentProjects.begin(), m_recentProjects.end(), proj) == m_recentProjects.end()) if (std::find(m_recentProjects.begin(), m_recentProjects.end(), proj) == m_recentProjects.end())
@ -682,6 +681,7 @@ void MainWindow::CloseProject()
if (m_story) if (m_story)
{ {
m_story->Clear(); m_story->Clear();
m_story.reset();
} }
m_resources.Clear(); m_resources.Clear();
@ -812,6 +812,11 @@ void MainWindow::DeleteResource(FilterIterator &it)
return m_resources.Delete(it); return m_resources.Delete(it);
} }
std::shared_ptr<BaseNode> MainWindow::CreateNode(const std::string &type)
{
return m_story->CreateNode(type);
}
void MainWindow::Build(bool compileonly) void MainWindow::Build(bool compileonly)
{ {
// 1. First compile nodes to assembly // 1. First compile nodes to assembly
@ -829,15 +834,19 @@ void MainWindow::Build(bool compileonly)
} }
} }
void MainWindow::DeleteNode(const std::string &id)
std::string MainWindow::GetNodeEntryLabel(const std::string &nodeId)
{ {
return m_nodeEditorWindow.GetNodeEntryLabel(nodeId); m_story->DeleteNode(id);
}
void MainWindow::DeleteLink(std::shared_ptr<Connection> c)
{
m_story->DeleteLink(c);
} }
std::list<std::shared_ptr<Connection>> MainWindow::GetNodeConnections(const std::string &nodeId) std::list<std::shared_ptr<Connection>> MainWindow::GetNodeConnections(const std::string &nodeId)
{ {
return m_nodeEditorWindow.GetNodeConnections(nodeId); return m_story->GetNodeConnections(nodeId);
} }
bool MainWindow::CompileToAssembler() bool MainWindow::CompileToAssembler()
@ -846,7 +855,7 @@ bool MainWindow::CompileToAssembler()
// FIXME // FIXME
// 2. Generate the assembly code from the model // 2. Generate the assembly code from the model
bool ret = m_nodeEditorWindow.Build(m_currentCode); bool ret = m_story->Build(m_currentCode);
// Add global functions // Add global functions
if (ret) if (ret)

View file

@ -134,9 +134,13 @@ private:
virtual void ClearResources() override; virtual void ClearResources() override;
virtual std::pair<FilterIterator, FilterIterator> Resources() override; virtual std::pair<FilterIterator, FilterIterator> Resources() override;
virtual void DeleteResource(FilterIterator &it) override; virtual void DeleteResource(FilterIterator &it) override;
virtual std::shared_ptr<BaseNode> CreateNode(const std::string &type) override;
virtual void Build(bool compileonly) override; virtual void Build(bool compileonly) override;
virtual void DeleteNode(const std::string &id) override;
virtual void DeleteLink(std::shared_ptr<Connection> c) override;
virtual std::list<std::shared_ptr<Connection>> GetNodeConnections(const std::string &nodeId) override; virtual std::list<std::shared_ptr<Connection>> GetNodeConnections(const std::string &nodeId) override;
virtual std::string GetNodeEntryLabel(const std::string &nodeId) override;
virtual void Play() override; virtual void Play() override;
virtual void Ok() override; virtual void Ok() override;
virtual void Stop() override; virtual void Stop() override;

View file

@ -5,14 +5,18 @@
unsigned long BaseNodeWidget::s_nextId = 1; unsigned long BaseNodeWidget::s_nextId = 1;
BaseNodeWidget::BaseNodeWidget(const std::string &type, IStoryManager &proj) BaseNodeWidget::BaseNodeWidget(IStoryManager &manager, std::shared_ptr<BaseNode> base)
: BaseNode(type) : m_manager(manager)
, m_story(proj) , m_base(base)
{ {
// m_id = UUID().String();
m_node = std::make_unique<Node>(GetNextId(), ""); // ImGui internal ID m_node = std::make_unique<Node>(GetNextId(), ""); // ImGui internal ID
} }
BaseNodeWidget::~BaseNodeWidget()
{
std::cout << "Deleted node widget" << std::endl;
}
void BaseNodeWidget::AddInput() void BaseNodeWidget::AddInput()
{ {
m_node->Inputs.emplace_back(GetNextId(), "", PinType::Flow); m_node->Inputs.emplace_back(GetNextId(), "", PinType::Flow);
@ -60,9 +64,12 @@ void BaseNodeWidget::FrameStart()
if (m_firstFrame) if (m_firstFrame)
{ {
// Use the parent node position, the one saved in the JSON project if (m_base)
// FIXME: find a better way to do that? {
ed::SetNodePosition(m_node->ID, ImVec2(BaseNode::GetX(), BaseNode::GetY())); // Use the parent node position, the one saved in the JSON project
// FIXME: find a better way to do that?
ed::SetNodePosition(m_node->ID, ImVec2(m_base->GetX(), m_base->GetY()));
}
} }
m_firstFrame = false; m_firstFrame = false;
} }

View file

@ -95,24 +95,16 @@ struct Link
* @brief Basically a wrapper class around ImGuiNodeEditor Node structure * @brief Basically a wrapper class around ImGuiNodeEditor Node structure
* *
*/ */
class BaseNodeWidget : public BaseNode class BaseNodeWidget
{ {
public: public:
struct NodePosition BaseNodeWidget(IStoryManager &manager, std::shared_ptr<BaseNode> base);
{ virtual ~BaseNodeWidget();
float x;
float y;
};
BaseNodeWidget(const std::string &type, IStoryManager &proj);
virtual void Initialize(); virtual void Initialize();
virtual void Draw() = 0; virtual void Draw() = 0;
virtual void DrawProperties() = 0; virtual void DrawProperties() = 0;
virtual std::string GenerateConstants() = 0;
virtual std::string Build() = 0;
virtual std::string GetEntryLabel() = 0;
void FrameStart(); void FrameStart();
@ -201,14 +193,16 @@ public:
void SetOutputs(uint32_t num); void SetOutputs(uint32_t num);
void DeleteOutput(); void DeleteOutput();
std::shared_ptr<BaseNode> Base() { return m_base; }
private: private:
IStoryManager &m_story; IStoryManager &m_manager;
std::shared_ptr<BaseNode> m_base;
std::unique_ptr<Node> m_node; std::unique_ptr<Node> m_node;
bool m_firstFrame{true}; bool m_firstFrame{true};
static unsigned long s_nextId; static unsigned long s_nextId;
}; };

View file

@ -1,339 +0,0 @@
#include <sstream>
#include "media_node.h"
namespace ed = ax::NodeEditor;
#include "IconsMaterialDesignIcons.h"
#include "story_project.h"
MediaNode::MediaNode(const std::string &title, IStoryManager &proj)
: BaseNodeWidget(title, proj)
, m_story(proj)
{
// Create defaut one input and one output
AddInput();
AddOutputs(1);
std::string widgetId = std::to_string(GetInternalId()); // Make widget unique by using the node ID
m_buttonUniqueName = "Play " ICON_MDI_PLAY "##id" + widgetId;
}
void MediaNode::Draw()
{
BaseNodeWidget::FrameStart();
static ImGuiTableFlags flags = ImGuiTableFlags_Borders |
ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_SizingFixedFit;
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(10.0f, 10.0f));
if (ImGui::BeginTable("table1", 1, flags))
{
ImGui::TableNextRow();
ImU32 bg_color = ImGui::GetColorU32(ImVec4(0.3f, 0.3f, 0.7f, 1.0f));
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, bg_color);
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted("Media node");
ImGui::EndTable();
}
ImGui::PopStyleVar();
if (m_image.Valid())
{
ImGui::Image(m_image.texture, ImVec2(320, 240));
}
else
{
ImGui::Dummy(ImVec2(320, 240));
}
// Use AlignTextToFramePadding() to align text baseline to the baseline of framed elements
// (otherwise a Text+SameLine+Button sequence will have the text a little too high by default)
// Use AlignTextToFramePadding() to align text baseline to the baseline of framed elements
// (otherwise a Text+SameLine+Button sequence will have the text a little too high by default)
ImGui::AlignTextToFramePadding();
ImGui::Text("Outputs:");
ImGui::SameLine();
// Arrow buttons with Repeater
uint32_t counter = Outputs();
float spacing = ImGui::GetStyle().ItemInnerSpacing.x;
ImGui::PushButtonRepeat(true);
std::string leftSingle = "##left" + GetId();
if (ImGui::ArrowButton(leftSingle.c_str(), ImGuiDir_Left)) { if (counter > 1) counter--; }
ImGui::SameLine(0.0f, spacing);
std::string rightSingle = "##right" + GetId();
if (ImGui::ArrowButton(rightSingle.c_str(), ImGuiDir_Right))
{
counter++;
}
ImGui::PopButtonRepeat();
ImGui::SameLine();
ImGui::Text("%d", counter);
SetOutputs(counter);
DrawPins();
BaseNodeWidget::FrameEnd();
}
/*
"internal-data": {
"image": "fairy.png",
"sound": "la_fee_luminelle.mp3"
},
*/
void MediaNode::Initialize()
{
BaseNodeWidget::Initialize();
nlohmann::json j = GetInternalData();
SetImage(j["image"].get<std::string>());
SetSound(j["sound"].get<std::string>());
}
void MediaNode::StoreInternalData()
{
nlohmann::json j;
j["image"] = m_image.name;
j["sound"] = m_soundName;
SetInternalData(j);
}
void MediaNode::DrawProperties()
{
ImGui::AlignTextToFramePadding();
ImGui::Text("Image");
ImGui::SameLine();
ImGui::Text("%s", m_image.name.c_str());
ImGui::SameLine();
static bool isImage = true;
if (ImGui::Button("Select...##image")) {
ImGui::OpenPopup("popup_button");
isImage = true;
}
ImGui::SameLine();
if (ImGui::Button(ICON_MDI_CLOSE_BOX_OUTLINE "##delimage")) {
SetImage("");
}
ImGui::AlignTextToFramePadding();
ImGui::Text("Sound");
ImGui::SameLine();
ImGui::Text("%s", m_soundName.c_str());
ImGui::SameLine();
if (ImGui::Button(m_buttonUniqueName.c_str()))
{
m_story.PlaySoundFile(m_soundPath);
}
ImGui::SameLine();
if (ImGui::Button("Select...##sound")) {
ImGui::OpenPopup("popup_button");
isImage = false;
}
ImGui::SameLine();
if (ImGui::Button(ICON_MDI_CLOSE_BOX_OUTLINE "##delsound")) {
SetSound("");
}
// This is the actual popup Gui drawing section.
if (ImGui::BeginPopup("popup_button")) {
ImGui::SeparatorText(isImage ? "Images" : "Sounds");
auto [filtreDebut, filtreFin] = isImage ? m_story.Images() : m_story.Sounds();
int n = 0;
for (auto it = filtreDebut; it != filtreFin; ++it, n++)
{
if (ImGui::Selectable((*it)->file.c_str()))
{
if (isImage)
{
SetImage((*it)->file);
}
else
{
SetSound((*it)->file);
}
}
}
ImGui::EndPopup(); // Note this does not do anything to the popup open/close state. It just terminates the content declaration.
}
}
void MediaNode::SetImage(const std::string &f)
{
m_image.name = f;
m_image.Load(m_story.BuildFullAssetsPath(f));
StoreInternalData();
}
void MediaNode::SetSound(const std::string &f)
{
m_soundName = f;
m_soundPath = m_story.BuildFullAssetsPath(m_soundName);
StoreInternalData();
}
std::string MediaNode::ChoiceLabel() const
{
std::stringstream ss;
ss << "mediaChoice" << std::setw(4) << std::setfill('0') << GetId();
return ss.str();
}
std::string MediaNode::GetEntryLabel()
{
std::stringstream ss;
ss << ".mediaEntry" << std::setw(4) << std::setfill('0') << GetId();
return ss.str();
}
std::string MediaNode::GenerateConstants()
{
std::string s;
if (m_image.name.size() > 0)
{
s = StoryProject::FileToConstant(m_image.name, ".qoi"); // FIXME: Generate the extension setup in user option of output format
}
if (m_soundName.size() > 0)
{
s += StoryProject::FileToConstant(m_soundName, ".wav"); // FIXME: Generate the extension setup in user option of output format
}
int nb_out_conns = Outputs();
if (nb_out_conns > 1)
{
// Generate choice table if needed (out ports > 1)
std::stringstream ss;
std::string label = ChoiceLabel();
ss << "$" << label
<< " DC32, "
<< nb_out_conns << ", ";
std::list<std::shared_ptr<Connection>> conns = m_story.GetNodeConnections(GetId());
int i = 0;
for (auto & c : conns)
{
std::stringstream ssChoice;
// On va chercher le label d'entrée du noeud connecté à l'autre bout
ss << m_story.GetNodeEntryLabel(c->inNodeId);
if (i < (nb_out_conns - 1))
{
ss << ", ";
}
else
{
ss << "\n";
}
i++;
}
s += ss.str();
}
return s;
}
std::string MediaNode::Build()
{
std::stringstream ss;
int nb_out_conns = Outputs();
ss << R"(; ---------------------------- )"
<< GetTitle()
<< " Type: "
<< (nb_out_conns == 0 ? "End" : nb_out_conns == 1 ? "Transition" : "Choice")
<< "\n";
std::string image = StoryProject::RemoveFileExtension(m_image.name);
std::string sound = StoryProject::RemoveFileExtension(m_soundName);
// Le label de ce noeud est généré de la façon suivante :
// "media" + Node ID + id du noeud parent. Si pas de noeud parent, alors rien
ss << GetEntryLabel() << ":\n";
if (image.size() > 0)
{
ss << "lcons r0, $" << image << "\n";
}
else
{
ss << "lcons r0, 0\n";
}
if (sound.size() > 0)
{
ss << "lcons r1, $" << sound << "\n";
}
else
{
ss << "lcons r1, 0\n";
}
// Call the media executor (image, sound)
ss << "syscall 1\n";
// Check output connections number
// == 0: end node : generate halt
// == 1: transition node : image + sound on demand, jump directly to the other node when OK
// > 1 : choice node : call the node choice manager
if (nb_out_conns == 0) // End node
{
ss << "halt\n";
}
else if (nb_out_conns == 1) // it is a transition node
{
std::list<std::shared_ptr<Connection>> conns = m_story.GetNodeConnections(GetId());
for (auto c : conns)
{
if (c->outNodeId == GetId())
{
// On place dans R0 le prochain noeud à exécuter en cas de OK
ss << "lcons r0, "
<< m_story.GetNodeEntryLabel(c->inNodeId) << "\n"
<< "ret\n";
}
}
}
else // Choice node
{
ss << "lcons r0, $" << ChoiceLabel() << "\n"
<< "jump .media ; no return possible, so a jump is enough";
}
return ss.str();
}

View file

@ -0,0 +1,204 @@
#include <sstream>
#include "media_node_widget.h"
namespace ed = ax::NodeEditor;
#include "IconsMaterialDesignIcons.h"
#include "story_project.h"
MediaNodeWidget::MediaNodeWidget(IStoryManager &manager, std::shared_ptr<BaseNode> node)
: BaseNodeWidget(manager, node)
, m_manager(manager)
{
m_mediaNode = std::dynamic_pointer_cast<MediaNode>(node);
// Create defaut one input and one output
AddInput();
AddOutputs(1);
std::string widgetId = std::to_string(GetInternalId()); // Make widget unique by using the node ID
m_buttonUniqueName = "Play " ICON_MDI_PLAY "##id" + widgetId;
}
void MediaNodeWidget::Draw()
{
BaseNodeWidget::FrameStart();
static ImGuiTableFlags flags = ImGuiTableFlags_Borders |
ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_SizingFixedFit;
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(10.0f, 10.0f));
if (ImGui::BeginTable("table1", 1, flags))
{
ImGui::TableNextRow();
ImU32 bg_color = ImGui::GetColorU32(ImVec4(0.3f, 0.3f, 0.7f, 1.0f));
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, bg_color);
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted("Media node");
ImGui::EndTable();
}
ImGui::PopStyleVar();
if (m_image.Valid())
{
ImGui::Image(m_image.texture, ImVec2(320, 240));
}
else
{
ImGui::Dummy(ImVec2(320, 240));
}
// Use AlignTextToFramePadding() to align text baseline to the baseline of framed elements
// (otherwise a Text+SameLine+Button sequence will have the text a little too high by default)
// Use AlignTextToFramePadding() to align text baseline to the baseline of framed elements
// (otherwise a Text+SameLine+Button sequence will have the text a little too high by default)
ImGui::AlignTextToFramePadding();
ImGui::Text("Outputs:");
ImGui::SameLine();
// Arrow buttons with Repeater
uint32_t counter = Outputs();
float spacing = ImGui::GetStyle().ItemInnerSpacing.x;
ImGui::PushButtonRepeat(true);
std::string leftSingle = "##left" + m_mediaNode->GetId();
if (ImGui::ArrowButton(leftSingle.c_str(), ImGuiDir_Left)) { if (counter > 1) counter--; }
ImGui::SameLine(0.0f, spacing);
std::string rightSingle = "##right" + m_mediaNode->GetId();
if (ImGui::ArrowButton(rightSingle.c_str(), ImGuiDir_Right))
{
counter++;
}
ImGui::PopButtonRepeat();
ImGui::SameLine();
ImGui::Text("%d", counter);
SetOutputs(counter);
DrawPins();
BaseNodeWidget::FrameEnd();
}
/*
"internal-data": {
"image": "fairy.png",
"sound": "la_fee_luminelle.mp3"
},
*/
void MediaNodeWidget::Initialize()
{
BaseNodeWidget::Initialize();
nlohmann::json j = m_mediaNode->GetInternalData();
SetImage(j["image"].get<std::string>());
SetSound(j["sound"].get<std::string>());
}
void MediaNodeWidget::StoreInternalData()
{
nlohmann::json j;
j["image"] = m_mediaNode->image;
j["sound"] = m_mediaNode->sound;
m_mediaNode->SetInternalData(j);
}
void MediaNodeWidget::DrawProperties()
{
ImGui::AlignTextToFramePadding();
ImGui::Text("Image");
ImGui::SameLine();
ImGui::Text("%s", m_mediaNode->image.c_str());
ImGui::SameLine();
static bool isImage = true;
if (ImGui::Button("Select...##image")) {
ImGui::OpenPopup("popup_button");
isImage = true;
}
ImGui::SameLine();
if (ImGui::Button(ICON_MDI_CLOSE_BOX_OUTLINE "##delimage")) {
SetImage("");
}
ImGui::AlignTextToFramePadding();
ImGui::Text("Sound");
ImGui::SameLine();
ImGui::Text("%s", m_mediaNode->sound.c_str());
ImGui::SameLine();
if (ImGui::Button(m_buttonUniqueName.c_str()))
{
m_manager.PlaySoundFile(m_soundPath);
}
ImGui::SameLine();
if (ImGui::Button("Select...##sound")) {
ImGui::OpenPopup("popup_button");
isImage = false;
}
ImGui::SameLine();
if (ImGui::Button(ICON_MDI_CLOSE_BOX_OUTLINE "##delsound")) {
SetSound("");
}
// This is the actual popup Gui drawing section.
if (ImGui::BeginPopup("popup_button")) {
ImGui::SeparatorText(isImage ? "Images" : "Sounds");
auto [filtreDebut, filtreFin] = isImage ? m_manager.Images() : m_manager.Sounds();
int n = 0;
for (auto it = filtreDebut; it != filtreFin; ++it, n++)
{
if (ImGui::Selectable((*it)->file.c_str()))
{
if (isImage)
{
SetImage((*it)->file);
}
else
{
SetSound((*it)->file);
}
}
}
ImGui::EndPopup(); // Note this does not do anything to the popup open/close state. It just terminates the content declaration.
}
}
void MediaNodeWidget::SetImage(const std::string &f)
{
m_mediaNode->image = f;
m_image.Load(m_manager.BuildFullAssetsPath(f));
StoreInternalData();
}
void MediaNodeWidget::SetSound(const std::string &f)
{
m_mediaNode->sound = f;
m_soundPath = m_manager.BuildFullAssetsPath(m_mediaNode->sound);
StoreInternalData();
}

View file

@ -7,34 +7,33 @@
#include "base_node_widget.h" #include "base_node_widget.h"
#include "i_story_manager.h" #include "i_story_manager.h"
#include "i_story_project.h"
#include "gui.h" #include "gui.h"
#include <imgui_node_editor.h> #include <imgui_node_editor.h>
#include "media_node.h"
class MediaNodeWidget : public BaseNodeWidget
class MediaNode : public BaseNodeWidget
{ {
public: public:
MediaNode(const std::string &title, IStoryManager &proj); MediaNodeWidget(IStoryManager &manager, std::shared_ptr<BaseNode> node);
void Draw() override; void Draw() override;
virtual void DrawProperties() override; virtual void DrawProperties() override;
virtual std::string Build() override;
virtual std::string GetEntryLabel() override;
virtual std::string GenerateConstants() override;
virtual void Initialize() override; virtual void Initialize() override;
private: private:
IStoryManager &m_story; IStoryManager &m_manager;
std::shared_ptr<MediaNode> m_mediaNode;
Gui::Image m_image; Gui::Image m_image;
std::string m_soundName;
std::string m_soundPath; std::string m_soundPath;
std::string m_buttonUniqueName; std::string m_buttonUniqueName;
void SetImage(const std::string &f); void SetImage(const std::string &f);
void SetSound(const std::string &f); void SetSound(const std::string &f);
std::string ChoiceLabel() const;
void StoreInternalData(); void StoreInternalData();
}; };

View file

@ -9,7 +9,7 @@
#include <sstream> #include <sstream>
#include "IconsFontAwesome5_c.h" #include "IconsFontAwesome5_c.h"
#include "media_node.h" #include "media_node_widget.h"
#include "gui.h" #include "gui.h"
#include "uuid.h" #include "uuid.h"
@ -21,12 +21,12 @@ if (!(x)) { \
#include "json.hpp" #include "json.hpp"
NodeEditorWindow::NodeEditorWindow(IStoryManager &proj) NodeEditorWindow::NodeEditorWindow(IStoryManager &manager)
: WindowBase("Node editor") : WindowBase("Node editor")
, m_story(proj) , m_manager(manager)
{ {
registerNode<MediaNode>("media-node"); registerNode<MediaNodeWidget>("media-node");
} }
NodeEditorWindow::~NodeEditorWindow() NodeEditorWindow::~NodeEditorWindow()
@ -46,21 +46,17 @@ void NodeEditorWindow::Initialize()
void NodeEditorWindow::Clear() void NodeEditorWindow::Clear()
{ {
m_nodes.clear(); m_nodes.clear();
m_links.clear();
m_story.reset();
} }
std::string NodeEditorWindow::GenerateNodeId()
{
return Uuid().String();
}
ed::PinId NodeEditorWindow::GetInputPin(const std::string &modelNodeId, int pinIndex) ed::PinId NodeEditorWindow::GetInputPin(const std::string &modelNodeId, int pinIndex)
{ {
ed::PinId id = 0; ed::PinId id = 0;
for (auto & n : m_nodes) for (auto & n : m_nodes)
{ {
if (n->GetId() == modelNodeId) if (n->Base()->GetId() == modelNodeId)
{ {
id = n->GetInputPinAt(pinIndex); id = n->GetInputPinAt(pinIndex);
break; break;
@ -81,7 +77,7 @@ ed::PinId NodeEditorWindow::GetOutputPin(const std::string &modelNodeId, int pin
for (auto & n : m_nodes) for (auto & n : m_nodes)
{ {
if (n->GetId() == modelNodeId) if (n->Base()->GetId() == modelNodeId)
{ {
id = n->GetOutputPinAt(pinIndex); id = n->GetOutputPinAt(pinIndex);
break; break;
@ -96,92 +92,59 @@ ed::PinId NodeEditorWindow::GetOutputPin(const std::string &modelNodeId, int pin
return id; return id;
} }
void NodeEditorWindow::Load(const nlohmann::json &model) void NodeEditorWindow::Load(std::shared_ptr<StoryProject> story)
{ {
try { m_story = story;
nlohmann::json nodesJsonArray = model["nodes"]; if (m_story)
{
try {
BaseNodeWidget::InitId(); BaseNodeWidget::InitId();
m_nodes.clear(); m_nodes.clear();
m_links.clear(); m_links.clear();
for (auto& element : nodesJsonArray) { auto [node_begin, node_end] = m_story->Nodes();
std::string type = element["type"].get<std::string>();
auto n = createNode(type, m_story); for (auto it = node_begin; it != node_end; ++it)
if (n)
{ {
n->FromJson(element); auto n = CreateNodeWidget((*it)->GetType(), m_manager, (*it));
n->Initialize(); if (n)
m_nodes.push_back(n);
}
else
{
throw std::logic_error(std::string("No registered model with name ") + type);
}
}
std::cout << model.dump(4) << std::endl;
// Ici on reste flexible sur les connexions, cela permet de créer éventuellement des
// projets sans fils (bon, l'élément devrait quand même exister dans le JSON)
if (model.contains("connections"))
{
nlohmann::json connectionJsonArray = model["connections"];
// key: node UUID, value: output counts
std::map<std::string, int> outputCounts;
for (auto& connection : connectionJsonArray)
{
Connection model = connection.get<Connection>();
if (outputCounts.count(model.outNodeId) > 0)
{ {
outputCounts[model.outNodeId]++; n->Initialize();
n->SetOutputs(m_story->OutputsCount((*it)->GetId())); // il faut que les noeuds aient une bonne taille de outputs avant de créer les liens
m_nodes.push_back(n);
} }
else else
{ {
outputCounts[model.outNodeId] = 1; throw std::logic_error(std::string("No registered model with name ") + (*it)->GetType());
} }
}
auto [link_begin, link_end] = m_story->Links();
// Adjust the number of outputs for all nodes for (auto it = link_begin; it != link_end; ++it)
for (auto n : m_nodes) {
{ CreateLink(*it,
if (outputCounts.count(n->GetId()) > 0) GetInputPin((*it)->inNodeId, (*it)->inPortIndex),
{ GetOutputPin((*it)->outNodeId, (*it)->outPortIndex));
n->SetOutputs(outputCounts[n->GetId()]);
}
}
CreateLink(model,
GetInputPin(model.inNodeId, model.inPortIndex),
GetOutputPin(model.outNodeId, model.outPortIndex));
} }
m_loaded = true;
}
catch(std::exception &e)
{
std::cout << "(NodeEditorWindow::Load) " << e.what() << std::endl;
} }
m_loaded = true;
}
catch(std::exception &e)
{
std::cout << "(NodeEditorWindow::Load) " << e.what() << std::endl;
} }
} }
void NodeEditorWindow::CreateLink(const Connection &model, ed::PinId inId, ed::PinId outId) void NodeEditorWindow::CreateLink(std::shared_ptr<Connection> model, ed::PinId inId, ed::PinId outId)
{ {
auto conn = std::make_shared<LinkInfo>(); auto conn = std::make_shared<LinkInfo>();
*conn->model = model; conn->model = model;
// ImGui stuff for links // ImGui stuff for links
conn->ed_link->Id = BaseNodeWidget::GetNextId(); conn->ed_link->Id = BaseNodeWidget::GetNextId();
@ -192,159 +155,30 @@ void NodeEditorWindow::CreateLink(const Connection &model, ed::PinId inId, ed::P
m_links.push_back(conn); m_links.push_back(conn);
} }
void NodeEditorWindow::Save(nlohmann::json &model)
std::shared_ptr<Connection> NodeEditorWindow::LinkToModel(ed::PinId InputId, ed::PinId OutputId)
{ {
ed::SetCurrentEditor(m_context); auto c = std::make_shared<Connection>();
// Save nodes
nlohmann::json nodes = nlohmann::json::array();
for (const auto & n : m_nodes)
{
nodes.push_back(n->ToJson());
}
model["nodes"] = nodes;
// Save links
nlohmann::json connections = nlohmann::json::array();
for (const auto& linkInfo : m_links)
{
nlohmann::json c;
Connection cnx = LinkToModel(linkInfo->ed_link->InputId, linkInfo->ed_link->OutputId);
c["outNodeId"] = cnx.outNodeId;
c["outPortIndex"] = cnx.outPortIndex;
c["inNodeId"] = cnx.inNodeId;
c["inPortIndex"] = cnx.inPortIndex;
connections.push_back(c);
}
model["connections"] = connections;
ed::SetCurrentEditor(nullptr);
}
Connection NodeEditorWindow::LinkToModel(ed::PinId InputId, ed::PinId OutputId)
{
Connection c;
int index; int index;
for (const auto & n : m_nodes) for (const auto & n : m_nodes)
{ {
if (n->HasOnputPinId(OutputId, index)) if (n->HasOnputPinId(OutputId, index))
{ {
c.outNodeId = n->GetId(); c->outNodeId = n->Base()->GetId();
c.outPortIndex = index; c->outPortIndex = index;
} }
if (n->HasInputPinId(InputId, index)) if (n->HasInputPinId(InputId, index))
{ {
c.inNodeId = n->GetId(); c->inNodeId = n->Base()->GetId();
c.inPortIndex = index; c->inPortIndex = index;
} }
} }
return c; return c;
} }
std::string NodeEditorWindow::FindFirstNode() const
{
std::string id;
// First node is the one without connection on its input port
for (const auto & n : m_nodes)
{
bool foundConnection = false;
for (const auto& l : m_links)
{
if (l->model->inNodeId == n->GetId())
{
foundConnection = true;
}
}
if (!foundConnection)
{
id = n->GetId();
m_story.Log("First node is: " + id);
break;
}
}
return id;
}
bool NodeEditorWindow::Build(std::string &codeStr)
{
std::stringstream code;
ed::SetCurrentEditor(m_context);
std::stringstream chip32;
std::string firstNode = FindFirstNode();
if (firstNode == "")
{
m_story.Log("First node not found, there must be only one node with a free input.", true);
return false;
}
code << "\tjump " << GetNodeEntryLabel(firstNode) << "\r\n";
// First generate all constants
for (const auto & n : m_nodes)
{
code << n->GenerateConstants() << "\n";
}
for (const auto & n : m_nodes)
{
code << n->Build() << "\n";
}
ed::SetCurrentEditor(nullptr);
codeStr = code.str();
return true;
}
std::list<std::shared_ptr<Connection>> NodeEditorWindow::GetNodeConnections(const std::string &nodeId)
{
std::list<std::shared_ptr<Connection>> c;
ed::SetCurrentEditor(m_context);
for (const auto & l : m_links)
{
if (l->model->outNodeId == nodeId)
{
c.push_back(l->model);
}
}
ed::SetCurrentEditor(nullptr);
return c;
}
std::string NodeEditorWindow::GetNodeEntryLabel(const std::string &nodeId)
{
std::string label;
ed::SetCurrentEditor(m_context);
for (const auto & n : m_nodes)
{
if (n->GetId() == nodeId)
{
label = n->GetEntryLabel();
break;
}
}
ed::SetCurrentEditor(nullptr);
return label;
}
std::shared_ptr<BaseNodeWidget> NodeEditorWindow::GetSelectedNode() std::shared_ptr<BaseNodeWidget> NodeEditorWindow::GetSelectedNode()
{ {
@ -417,9 +251,10 @@ void NodeEditorWindow::Draw()
// ed::AcceptNewItem() return true when user release mouse button. // ed::AcceptNewItem() return true when user release mouse button.
if (ed::AcceptNewItem()) if (ed::AcceptNewItem())
{ {
Connection model = LinkToModel(inputPinId, outputPinId); auto c = LinkToModel(inputPinId, outputPinId);
m_story->AddConnection(c);
CreateLink(model, inputPinId, outputPinId); CreateLink(c, inputPinId, outputPinId);
// Draw new link. // Draw new link.
ed::Link(m_links.back()->ed_link->Id, inputPinId, outputPinId); ed::Link(m_links.back()->ed_link->Id, inputPinId, outputPinId);
@ -442,9 +277,13 @@ void NodeEditorWindow::Draw()
{ {
if (ed::AcceptDeletedItem()) if (ed::AcceptDeletedItem())
{ {
auto id = std::find_if(m_nodes.begin(), m_nodes.end(), [nodeId](std::shared_ptr<BaseNodeWidget> node) { return node->GetInternalId() == nodeId.Get(); }); auto it = std::find_if(m_nodes.begin(), m_nodes.end(), [nodeId](std::shared_ptr<BaseNodeWidget> node) { return node->GetInternalId() == nodeId.Get(); });
if (id != m_nodes.end()) if (it != m_nodes.end())
m_nodes.erase(id); {
// First delete model, then current entry
m_manager.DeleteNode((*it)->Base()->GetId());
m_nodes.erase(it);
}
} }
} }
@ -457,12 +296,13 @@ void NodeEditorWindow::Draw()
if (ed::AcceptDeletedItem()) if (ed::AcceptDeletedItem())
{ {
m_links.erase(std::remove_if(m_links.begin(), auto it = std::find_if(m_links.begin(), m_links.end(), [deletedLinkId](std::shared_ptr<LinkInfo> inf) { return inf->ed_link->Id == deletedLinkId; });
m_links.end(), if (it != m_links.end())
[deletedLinkId](std::shared_ptr<LinkInfo> inf) {
{ // First delete model, then current entry
return inf->ed_link->Id == deletedLinkId; m_manager.DeleteLink((*it)->model);
})); m_links.erase(it);
}
} }
// You may reject link deletion by calling: // You may reject link deletion by calling:
@ -486,13 +326,16 @@ void NodeEditorWindow::Draw()
Node* node = nullptr; Node* node = nullptr;
if (ImGui::MenuItem("Media Node")) if (ImGui::MenuItem("Media Node"))
{ {
auto n = createNode("media-node", m_story); auto base = m_manager.CreateNode("media-node");
if (n) if (base)
{ {
n->SetId(GenerateNodeId()); auto n = CreateNodeWidget(base->GetType(), m_manager, base);
n->SetPosition(newNodePostion.x, newNodePostion.y); if (n)
n->Initialize(); {
m_nodes.push_back(n); n->Base()->SetPosition(newNodePostion.x, newNodePostion.y);
n->Initialize();
m_nodes.push_back(n);
}
} }
} }

View file

@ -9,6 +9,7 @@
#include "window_base.h" #include "window_base.h"
#include "i_story_manager.h" #include "i_story_manager.h"
#include "json.hpp" #include "json.hpp"
#include "story_project.h"
namespace ed = ax::NodeEditor; namespace ed = ax::NodeEditor;
@ -46,28 +47,24 @@ public:
std::shared_ptr<Connection> model; std::shared_ptr<Connection> model;
}; };
NodeEditorWindow(IStoryManager &proj); NodeEditorWindow(IStoryManager &manager);
~NodeEditorWindow(); ~NodeEditorWindow();
virtual void Draw() override; virtual void Draw() override;
void Initialize(); void Initialize();
void Clear(); void Clear();
void Load(const nlohmann::json &model); void Load(std::shared_ptr<StoryProject> story);
void Save(nlohmann::json &model);
bool Build(std::string &codeStr);
std::list<std::shared_ptr<Connection> > GetNodeConnections(const std::string &nodeId);
std::string GetNodeEntryLabel(const std::string &nodeId);
std::shared_ptr<BaseNodeWidget> GetSelectedNode(); std::shared_ptr<BaseNodeWidget> GetSelectedNode();
private: private:
IStoryManager &m_story; IStoryManager &m_manager;
ed::EditorContext* m_context = nullptr;
bool m_loaded{false}; bool m_loaded{false};
// key: Id ed::EditorContext* m_context = nullptr;
std::shared_ptr<StoryProject> m_story;
std::list<std::shared_ptr<BaseNodeWidget>> m_nodes; std::list<std::shared_ptr<BaseNodeWidget>> m_nodes;
std::list<std::shared_ptr<LinkInfo>> m_links; // List of live links. It is dynamic unless you want to create read-only view over nodes. std::list<std::shared_ptr<LinkInfo>> m_links; // List of live links. It is dynamic unless you want to create read-only view over nodes.
void ToolbarUI(); void ToolbarUI();
@ -89,12 +86,12 @@ private:
template<class NodeType> template<class NodeType>
struct Factory { struct Factory {
static std::shared_ptr<BaseNodeWidget> create_func(const std::string &type, IStoryManager &proj) { static std::shared_ptr<BaseNodeWidget> create_func(IStoryManager &manager, std::shared_ptr<BaseNode> base) {
return std::make_shared<NodeType>(type, proj); return std::make_shared<NodeType>(manager, base);
} }
}; };
typedef std::shared_ptr<BaseNodeWidget> (*GenericCreator)(const std::string &type, IStoryManager &proj); typedef std::shared_ptr<BaseNodeWidget> (*GenericCreator)(IStoryManager &manager, std::shared_ptr<BaseNode> base);
typedef std::map<std::string, GenericCreator> Registry; typedef std::map<std::string, GenericCreator> Registry;
Registry m_registry; Registry m_registry;
@ -103,7 +100,7 @@ private:
m_registry.insert(typename Registry::value_type(key, Factory<Derived>::create_func)); m_registry.insert(typename Registry::value_type(key, Factory<Derived>::create_func));
} }
std::shared_ptr<BaseNodeWidget> createNode(const std::string& key, IStoryManager &proj) { std::shared_ptr<BaseNodeWidget> CreateNodeWidget(const std::string& key, IStoryManager &manager, std::shared_ptr<BaseNode> base) {
typename Registry::const_iterator i = m_registry.find(key); typename Registry::const_iterator i = m_registry.find(key);
if (i == m_registry.end()) { if (i == m_registry.end()) {
throw std::invalid_argument(std::string(__PRETTY_FUNCTION__) + throw std::invalid_argument(std::string(__PRETTY_FUNCTION__) +
@ -111,16 +108,13 @@ private:
} }
else else
{ {
return i->second(key, proj); return i->second(manager, base);
} }
} }
ed::PinId GetInputPin(const std::string &modelNodeId, int pinIndex); ed::PinId GetInputPin(const std::string &modelNodeId, int pinIndex);
ed::PinId GetOutputPin(const std::string &modelNodeId, int pinIndex); ed::PinId GetOutputPin(const std::string &modelNodeId, int pinIndex);
std::string FindFirstNode() const; void CreateLink(std::shared_ptr<Connection> model, ed::PinId inId, ed::PinId outId);
std::string GenerateNodeId(); std::shared_ptr<Connection> LinkToModel(ed::PinId InputId, ed::PinId OutputId);
void CreateLink(const Connection &model, ed::PinId inId, ed::PinId outId);
Connection LinkToModel(ed::PinId InputId, ed::PinId OutputId);
}; };

View file

@ -1,12 +1,29 @@
#include "base_node.h" #include "base_node.h"
#include "uuid.h"
#include <iostream> #include <iostream>
BaseNode::BaseNode(const std::string &type) BaseNode::BaseNode(const std::string &type)
{ {
m_type = type; m_type = type;
m_uuid = Uuid().String();
nlohmann::json obj{};
m_internal_data = obj;
} }
BaseNode::~BaseNode()
{
std::cout << "Deleted base node" << std::endl;
}
std::string BaseNode::GetEntryLabel(const std::string &id)
{
std::stringstream ss;
ss << ".nodeEntry" << std::setw(4) << std::setfill('0') << id;
return ss.str();
}
void BaseNode::FromJson(const nlohmann::json &j) void BaseNode::FromJson(const nlohmann::json &j)
{ {
try try
@ -16,6 +33,7 @@ void BaseNode::FromJson(const nlohmann::json &j)
m_type = j["type"].get<std::string>(); m_type = j["type"].get<std::string>();
m_title = j.value("title", "Default node"); m_title = j.value("title", "Default node");
nlohmann::json posJson = j["position"]; nlohmann::json posJson = j["position"];
SetPosition(posJson["x"].get<double>(), posJson["y"].get<double>()); SetPosition(posJson["x"].get<double>(), posJson["y"].get<double>());
} }
catch (std::exception& e) catch (std::exception& e)

View file

@ -2,11 +2,11 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include "json.hpp"
#include <random> #include <random>
#include <string> #include <string>
#include "json.hpp" #include "json.hpp"
#include "i_story_project.h"
class BaseNode class BaseNode
{ {
@ -18,6 +18,12 @@ public:
}; };
BaseNode(const std::string &type); BaseNode(const std::string &type);
virtual ~BaseNode();
static std::string GetEntryLabel(const std::string &id);
virtual std::string Build(IStoryProject &story, int nb_out_conns) = 0;
virtual std::string GenerateConstants(IStoryProject &story, int nb_out_conns) = 0;
void SetPosition(float x, float y); void SetPosition(float x, float y);
@ -50,6 +56,6 @@ private:
std::string m_uuid; std::string m_uuid;
NodePosition m_pos; NodePosition m_pos;
nlohmann::json m_internal_data; nlohmann::json m_internal_data{{}};
}; };

View file

@ -0,0 +1,14 @@
#pragma once
#include <string>
#include "i_story_manager.h"
#include "base_node.h"
class IScriptNode
{
public:
virtual ~IScriptNode() {}
virtual std::string Build(IStoryManager &story, int nb_out_conns, BaseNode &base) = 0;
virtual std::string GenerateConstants(IStoryManager &story, int nb_out_conns, BaseNode &base) = 0;
};

View file

@ -0,0 +1,137 @@
#include "media_node.h"
#include "story_project.h"
#include "connection.h"
static std::string ChoiceLabel(const std::string &id)
{
std::stringstream ss;
ss << "mediaChoice" << std::setw(4) << std::setfill('0') << id;
return ss.str();
}
MediaNode::MediaNode(const std::string &type)
: BaseNode(type)
{
}
std::string MediaNode::GenerateConstants(IStoryProject &story, int nb_out_conns)
{
std::string s;
if (image.size() > 0)
{
s = StoryProject::FileToConstant(image, ".qoi"); // FIXME: Generate the extension setup in user option of output format
}
if (sound.size() > 0)
{
s += StoryProject::FileToConstant(sound, ".wav"); // FIXME: Generate the extension setup in user option of output format
}
// Generate choice table if needed (out ports > 1)
std::stringstream ss;
std::string label = ChoiceLabel(GetId());
ss << "$" << label
<< " DC32, "
<< nb_out_conns << ", ";
std::list<std::shared_ptr<Connection>> conns = story.GetNodeConnections(GetId());
int i = 0;
for (auto & c : conns)
{
std::stringstream ssChoice;
// On va chercher le label d'entrée du noeud connecté à l'autre bout
ss << BaseNode::GetEntryLabel(c->inNodeId);
if (i < (nb_out_conns - 1))
{
ss << ", ";
}
else
{
ss << "\n";
}
i++;
}
s += ss.str();
return s;
}
std::string MediaNode::Build(IStoryProject &story, int nb_out_conns)
{
std::stringstream ss;
ss << R"(; ---------------------------- )"
<< GetTitle()
<< " Type: "
<< (nb_out_conns == 0 ? "End" : nb_out_conns == 1 ? "Transition" : "Choice")
<< "\n";
std::string image = StoryProject::RemoveFileExtension(image);
std::string sound = StoryProject::RemoveFileExtension(sound);
// Le label de ce noeud est généré de la façon suivante :
// "media" + Node ID + id du noeud parent. Si pas de noeud parent, alors rien
ss << BaseNode::GetEntryLabel(GetId()) << ":\n";
if (image.size() > 0)
{
ss << "lcons r0, $" << image << "\n";
}
else
{
ss << "lcons r0, 0\n";
}
if (sound.size() > 0)
{
ss << "lcons r1, $" << sound << "\n";
}
else
{
ss << "lcons r1, 0\n";
}
// Call the media executor (image, sound)
ss << "syscall 1\n";
// Check output connections number
// == 0: end node : generate halt
// == 1: transition node : image + sound on demand, jump directly to the other node when OK
// > 1 : choice node : call the node choice manager
if (nb_out_conns == 0) // End node
{
ss << "halt\n";
}
else if (nb_out_conns == 1) // it is a transition node
{
std::list<std::shared_ptr<Connection>> conns = story.GetNodeConnections(GetId());
for (auto c : conns)
{
if (c->outNodeId == GetId())
{
// On place dans R0 le prochain noeud à exécuter en cas de OK
ss << "lcons r0, "
<< BaseNode::GetEntryLabel(c->inNodeId) << "\n"
<< "ret\n";
}
}
}
else // Choice node
{
ss << "lcons r0, $" << ChoiceLabel(GetId()) << "\n"
<< "jump .media ; no return possible, so a jump is enough";
}
return ss.str();
}

View file

@ -0,0 +1,20 @@
#pragma once
#include <string>
#include "i_story_manager.h"
#include "base_node.h"
#include "i_script_node.h"
#include "i_story_project.h"
struct MediaNode : public BaseNode
{
MediaNode(const std::string &type);
virtual std::string Build(IStoryProject &story, int nb_out_conns) override;
virtual std::string GenerateConstants(IStoryProject &story, int nb_out_conns) override;
std::string image;
std::string sound;
};

View file

@ -25,7 +25,7 @@ void PropertiesWindow::Draw()
if (m_selectedNode) if (m_selectedNode)
{ {
static char buf1[32] = ""; ImGui::InputText("Title", buf1, 32); static char buf1[32] = ""; ImGui::InputText("Title", buf1, 32);
ImGui::Text("Node ID: %s", m_selectedNode->GetId().data()); ImGui::Text("Node ID: %s", m_selectedNode->Base()->GetId().data());
m_selectedNode->DrawProperties(); m_selectedNode->DrawProperties();
} }

View file

@ -7,6 +7,7 @@ class WindowBase
{ {
public: public:
WindowBase(const std::string &title); WindowBase(const std::string &title);
virtual ~WindowBase() {}
bool IsDisabled() const { bool IsDisabled() const {
return m_disabled; return m_disabled;