From 73aefa710083a0ae2842f21d2c83c6ba86523dea Mon Sep 17 00:00:00 2001 From: Anthony Rabine Date: Wed, 1 May 2024 00:09:15 +0200 Subject: [PATCH] Big overall architecture change: separation between display and story model --- shared/i_story_project.h | 16 + shared/library_manager.cpp | 9 +- {firmware/library => shared}/resource.h | 2 +- shared/story_project.cpp | 229 +++++++++++- shared/story_project.h | 66 +++- story-editor/CMakeLists.txt | 7 +- story-editor/src/emulator_window.cpp | 2 +- story-editor/src/emulator_window.h | 1 + story-editor/src/gui.h | 2 - story-editor/src/i_story_manager.h | 6 +- story-editor/src/importers/pack_archive.cpp | 55 ++- story-editor/src/library_window.cpp | 31 +- story-editor/src/main_window.cpp | 31 +- story-editor/src/main_window.h | 6 +- .../src/node_editor/base_node_widget.cpp | 21 +- .../src/node_editor/base_node_widget.h | 20 +- story-editor/src/node_editor/media_node.cpp | 339 ------------------ .../src/node_editor/media_node_widget.cpp | 204 +++++++++++ .../{media_node.h => media_node_widget.h} | 21 +- .../src/node_editor/node_editor_window.cpp | 301 ++++------------ .../src/node_editor/node_editor_window.h | 34 +- story-editor/src/node_engine/base_node.cpp | 20 +- story-editor/src/node_engine/base_node.h | 10 +- story-editor/src/node_engine/i_script_node.h | 14 + story-editor/src/node_engine/media_node.cpp | 137 +++++++ story-editor/src/node_engine/media_node.h | 20 ++ story-editor/src/properties_window.cpp | 2 +- story-editor/src/window_base.h | 1 + 28 files changed, 904 insertions(+), 703 deletions(-) create mode 100644 shared/i_story_project.h rename {firmware/library => shared}/resource.h (98%) delete mode 100644 story-editor/src/node_editor/media_node.cpp create mode 100644 story-editor/src/node_editor/media_node_widget.cpp rename story-editor/src/node_editor/{media_node.h => media_node_widget.h} (60%) create mode 100644 story-editor/src/node_engine/i_script_node.h create mode 100644 story-editor/src/node_engine/media_node.cpp create mode 100644 story-editor/src/node_engine/media_node.h diff --git a/shared/i_story_project.h b/shared/i_story_project.h new file mode 100644 index 0000000..09b1499 --- /dev/null +++ b/shared/i_story_project.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include "connection.h" + +class IStoryProject +{ +public: + virtual ~IStoryProject() {}; + virtual std::list> GetNodeConnections(const std::string &nodeId) = 0; + virtual int OutputsCount(const std::string &nodeId) = 0; +}; + + + diff --git a/shared/library_manager.cpp b/shared/library_manager.cpp index 3703663..7d09fef 100644 --- a/shared/library_manager.cpp +++ b/shared/library_manager.cpp @@ -131,7 +131,14 @@ void LibraryManager::Save() void LibraryManager::CopyToDevice(const std::string &outputDir) { std::thread myThread([&]() { - myThread.detach(); + myThread.detach(); + + std::cout << "Starting to copy elements" << std::endl; + + for (auto p : *this) + { + + } }); } diff --git a/firmware/library/resource.h b/shared/resource.h similarity index 98% rename from firmware/library/resource.h rename to shared/resource.h index bd57114..34cfc31 100644 --- a/firmware/library/resource.h +++ b/shared/resource.h @@ -77,7 +77,7 @@ private: class IResource { public: - virtual ~IResource(); + virtual ~IResource() {} }; diff --git a/shared/story_project.cpp b/shared/story_project.cpp index a0d4ef0..21f3602 100644 --- a/shared/story_project.cpp +++ b/shared/story_project.cpp @@ -6,10 +6,11 @@ #include #include "json.hpp" +#include "media_node.h" StoryProject::StoryProject() { - + registerNode("media-node"); } 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; } +void StoryProject::CopyToDevice(const std::string &outputDir) +{ + ResourceManager manager; + + Load(manager); + +} void StoryProject::New(const std::string &uuid, const std::string &library_path) { @@ -70,8 +78,217 @@ bool StoryProject::ParseStoryInformation(nlohmann::json &j) 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 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 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 const &n) { return n->GetId() == id; }); + + if ( it != m_nodes.end() ) + { + it->reset(); + m_nodes.erase(it); + } +} + +void StoryProject::DeleteLink(std::shared_ptr c) +{ + auto it = std::find_if(m_links.begin(), + m_links.end(), + [&c](std::shared_ptr 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(); + + 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 outputCounts; + + for (auto& connection : connectionJsonArray) + { + m_links.push_back(std::make_shared(connection.get())); + } + } + 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> StoryProject::GetNodeConnections(const std::string &nodeId) +{ + std::list> 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 { std::ifstream f(m_project_file_path); @@ -98,7 +315,7 @@ bool StoryProject::Load(nlohmann::json &model, ResourceManager &manager) if (j.contains("nodegraph")) { - model = j["nodegraph"]; + ModelFromJson(j["nodegraph"]); m_initialized = true; } } @@ -173,7 +390,7 @@ bool StoryProject::Load(nlohmann::json &model, ResourceManager &manager) return m_initialized; } -void StoryProject::Save(const nlohmann::json &model, ResourceManager &manager) +void StoryProject::Save(ResourceManager &manager) { nlohmann::json j; 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; } + nlohmann::json model; + ModelToJson(model); j["nodegraph"] = model; std::ofstream o(m_project_file_path); @@ -240,6 +459,8 @@ void StoryProject::Clear() m_working_dir = ""; m_project_file_path = ""; m_initialized = false; + m_nodes.clear(); + m_links.clear(); } void StoryProject::EraseString(std::string &theString, const std::string &toErase) diff --git a/shared/story_project.h b/shared/story_project.h index 47d5631..ae051a0 100644 --- a/shared/story_project.h +++ b/shared/story_project.h @@ -8,7 +8,9 @@ #include "json.hpp" #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 ... struct StoryNode @@ -42,7 +44,7 @@ struct StoryNode }; -struct StoryProject +struct StoryProject : public IStoryProject { public: @@ -51,6 +53,10 @@ public: StoryProject(); ~StoryProject(); + + bool *Selected() { + return &m_selected; + } /* std::vector m_nodes; @@ -60,14 +66,28 @@ public: StoryNode *m_tree; */ void New(const std::string &uuid, const std::string &library_path); - bool Load(nlohmann::json &model, ResourceManager &manager); - void Save(const nlohmann::json &model, ResourceManager &manager); + bool Build(std::string &codeStr); + bool Load(ResourceManager &manager); + void Save(ResourceManager &manager); void SaveBinary(const std::vector &m_program); 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 Clear(); + + std::pair>::iterator, std::list>::iterator> Nodes() { + return std::make_pair(m_nodes.begin(), m_nodes.end()); + } + + std::pair>::iterator, std::list>::iterator> Links() { + return std::make_pair(m_links.begin(), m_links.end()); + } + void Select(bool selected) { m_selected = selected; } bool IsSelected() const { return m_selected; } @@ -104,11 +124,21 @@ public: // Initialize with an existing project const bool IsInitialized() const { return m_initialized; } - static void EraseString(std::string &theString, const std::string &toErase); static std::string ToUpper(const std::string &input); bool ParseStoryInformation(nlohmann::json &j); + + // From IStoryProject + virtual std::list> GetNodeConnections(const std::string &nodeId) override; + std::string FindFirstNode() const; + virtual int OutputsCount(const std::string &nodeId) override; + + std::shared_ptr CreateNode(const std::string& type); + void AddConnection(std::shared_ptr c); + void DeleteNode(const std::string &id); + void DeleteLink(std::shared_ptr c); + private: // Project properties and location std::string m_name; /// human readable name @@ -121,6 +151,10 @@ private: std::filesystem::path m_assetsPath; + // Model in memory + std::list> m_links; + std::list> m_nodes; + bool m_initialized{false}; 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}; SoundFormat m_soundFormat{SND_FORMAT_WAV}; + + + template + struct Factory { + static std::shared_ptr create_func(const std::string &type) { + return std::make_shared(type); + } + }; + + typedef std::shared_ptr (*GenericCreator)(const std::string &type); + typedef std::map Registry; + Registry m_registry; + + template + void registerNode(const std::string& key) { + m_registry.insert(typename Registry::value_type(key, Factory::create_func)); + } + + + }; #endif // STORY_PROJECT_H + + diff --git a/story-editor/CMakeLists.txt b/story-editor/CMakeLists.txt index 77c6317..ec7b14c 100644 --- a/story-editor/CMakeLists.txt +++ b/story-editor/CMakeLists.txt @@ -171,12 +171,13 @@ set(SRCS src/node_engine/base_node.h 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.h - - src/node_editor/media_node.h - src/node_editor/media_node.cpp + src/node_editor/media_node_widget.h + src/node_editor/media_node_widget.cpp src/node_editor/base_node_widget.h src/node_editor/base_node_widget.cpp src/node_editor/node_editor_window.h diff --git a/story-editor/src/emulator_window.cpp b/story-editor/src/emulator_window.cpp index 88e8096..11494f4 100644 --- a/story-editor/src/emulator_window.cpp +++ b/story-editor/src/emulator_window.cpp @@ -109,6 +109,6 @@ void EmulatorWindow::ClearImage() void EmulatorWindow::SetImage(const std::string &image) { - m_image.name = image; + m_imageFileName = image; m_image.Load(m_story.BuildFullAssetsPath(image)); } diff --git a/story-editor/src/emulator_window.h b/story-editor/src/emulator_window.h index c7f790e..8560bca 100644 --- a/story-editor/src/emulator_window.h +++ b/story-editor/src/emulator_window.h @@ -18,5 +18,6 @@ public: private: IStoryManager &m_story; Gui::Image m_image; + std::string m_imageFileName; }; diff --git a/story-editor/src/gui.h b/story-editor/src/gui.h index b3f1fc8..0f87c06 100644 --- a/story-editor/src/gui.h +++ b/story-editor/src/gui.h @@ -18,8 +18,6 @@ public: int w; int h; - std::string name; - bool Valid() const { return (w && h); } diff --git a/story-editor/src/i_story_manager.h b/story-editor/src/i_story_manager.h index 8a596b5..269ac92 100644 --- a/story-editor/src/i_story_manager.h +++ b/story-editor/src/i_story_manager.h @@ -9,6 +9,7 @@ #include "resource.h" #include "connection.h" +#include "base_node.h" template struct Callback; @@ -46,8 +47,11 @@ public: // Node interaction virtual void Build(bool compileonly) = 0; + virtual std::shared_ptr CreateNode(const std::string &type) = 0; + virtual void DeleteNode(const std::string &id) = 0; + virtual void DeleteLink(std::shared_ptr c) = 0; virtual std::list> GetNodeConnections(const std::string &nodeId) = 0; - virtual std::string GetNodeEntryLabel(const std::string &nodeId) = 0; + virtual void Play() = 0; virtual void Ok() = 0; virtual void Stop() = 0; diff --git a/story-editor/src/importers/pack_archive.cpp b/story-editor/src/importers/pack_archive.cpp index 44ee92d..55f06a5 100644 --- a/story-editor/src/importers/pack_archive.cpp +++ b/story-editor/src/importers/pack_archive.cpp @@ -429,7 +429,6 @@ bool PackArchive::ImportStudioFormat(const std::string &fileName, const std::str nlohmann::json j = nlohmann::json::parse(f); StoryProject proj; ResourceManager res; - nlohmann::json model; if (j.contains("title")) { @@ -458,58 +457,53 @@ bool PackArchive::ImportStudioFormat(const std::string &fileName, const std::str // Key: actionNode, value: Stage UUID std::map stageActionLink; - nlohmann::json jnodes = nlohmann::json::array(); + for (const auto & n : j["stageNodes"]) { - nlohmann::json node; + auto node = proj.CreateNode("media-node"); - auto node_uuid = n["uuid"].get(); - node["uuid"] = node_uuid; - node["type"] = "media-node"; - node["position"] = n["position"]; + if (node) + { + auto node_uuid = n["uuid"].get(); + node->SetId(node_uuid); + node->SetPosition(n["position"]["x"].get(), n["position"]["y"].get()); + + nlohmann::json internalData; + auto img = n["image"]; + internalData["image"] = img.is_string() ? img.get() : ""; + auto audio = n["audio"]; + internalData["sound"] = audio.is_string() ? audio.get() : ""; - nlohmann::json internalData; - auto img = n["image"]; - internalData["image"] = img.is_string() ? img.get() : ""; - auto audio = n["audio"]; - internalData["sound"] = audio.is_string() ? audio.get() : ""; + node->SetInternalData(internalData); - node["internal-data"] = internalData; - - stageActionLink[n["okTransition"]["actionNode"]] = node_uuid; + stageActionLink[n["okTransition"]["actionNode"]] = node_uuid; + } /* "okTransition":{ "actionNode":"19d7328f-d0d2-4443-a7a2-25270dafe52c", "optionIndex":0 }, */ - - jnodes.push_back(node); } - model["nodes"] = jnodes; - - nlohmann::json connections = nlohmann::json::array(); - for (const auto & n : j["actionNodes"]) { std::string action_node_uuid = n["id"].get(); // le champs est "id" et non pas "uuid", pénible if (stageActionLink.count(action_node_uuid) > 0) { - int i = 0; for (const auto & m : n["options"]) { - nlohmann::json c; + auto c = std::make_shared(); - c["outNodeId"] = stageActionLink[action_node_uuid]; - c["outPortIndex"] = i; - c["inNodeId"] = m; // On prend le stage node; - c["inPortIndex"] = 0; + c->outNodeId = stageActionLink[action_node_uuid]; + c->outPortIndex = i; + c->inNodeId = m; // On prend le stage node; + c->inPortIndex = 0; i++; - connections.push_back(c); + proj.AddConnection(c); } } else @@ -517,11 +511,8 @@ bool PackArchive::ImportStudioFormat(const std::string &fileName, const std::str std::cout << "ActionNode UUID not found" << std::endl; } } - - model["connections"] = connections; - // Save on disk - proj.Save(model, res); + proj.Save(res); } } catch(std::exception &e) diff --git a/story-editor/src/library_window.cpp b/story-editor/src/library_window.cpp index cfcab78..c02d8d5 100644 --- a/story-editor/src/library_window.cpp +++ b/story-editor/src/library_window.cpp @@ -368,16 +368,6 @@ void LibraryWindow::Draw() WindowBase::BeginDraw(); 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")) { IGFD::FileDialogConfig config; @@ -388,6 +378,16 @@ void LibraryWindow::Draw() 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()) { ImGui::SameLine(); @@ -470,8 +470,7 @@ void LibraryWindow::Draw() ImGui::TableSetColumnIndex(2); ImGui::TableHeader(ImGui::TableGetColumnName(2)); - int internal_id = 1; - uint32_t row = 0; + int internal_id = 1; for (auto &p : m_libraryManager) { ImGui::TableNextColumn(); @@ -480,19 +479,15 @@ void LibraryWindow::Draw() ImGui::PushID(internal_id++); // Select - ImGui::TableNextColumn(); - bool state = p->IsSelected(); - ImGui::Checkbox("", &state); + ImGui::Checkbox("", p->Selected()); - 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é } - row++; ImGui::TableNextColumn(); - if (ImGui::SmallButton("Load")) { m_storyManager.OpenProject(p->GetUuid()); diff --git a/story-editor/src/main_window.cpp b/story-editor/src/main_window.cpp index fa70339..4a89be3 100644 --- a/story-editor/src/main_window.cpp +++ b/story-editor/src/main_window.cpp @@ -446,6 +446,8 @@ void MainWindow::NewProjectPopup() if (!std::filesystem::is_directory(projdir)) { + CloseProject(); + m_story = m_libraryManager.NewProject(); if (m_story) @@ -608,25 +610,22 @@ void MainWindow::ProjectPropertiesPopup() void MainWindow::SaveProject() { nlohmann::json model; - m_nodeEditorWindow.Save(model); - m_story->Save(model, m_resources); + m_story->Save(m_resources); } void MainWindow::OpenProject(const std::string &uuid) { CloseProject(); - nlohmann::json model; - m_story = m_libraryManager.GetStory(uuid); if (!m_story) { Log("Cannot find story: " + uuid); } - else if (m_story->Load(model, m_resources)) + else if (m_story->Load(m_resources)) { Log("Open project success"); - m_nodeEditorWindow.Load(model); + m_nodeEditorWindow.Load(m_story); auto proj = m_story->GetProjectFilePath(); // Add to recent if not exists if (std::find(m_recentProjects.begin(), m_recentProjects.end(), proj) == m_recentProjects.end()) @@ -682,6 +681,7 @@ void MainWindow::CloseProject() if (m_story) { m_story->Clear(); + m_story.reset(); } m_resources.Clear(); @@ -812,6 +812,11 @@ void MainWindow::DeleteResource(FilterIterator &it) return m_resources.Delete(it); } +std::shared_ptr MainWindow::CreateNode(const std::string &type) +{ + return m_story->CreateNode(type); +} + void MainWindow::Build(bool compileonly) { // 1. First compile nodes to assembly @@ -829,15 +834,19 @@ void MainWindow::Build(bool compileonly) } } - -std::string MainWindow::GetNodeEntryLabel(const std::string &nodeId) +void MainWindow::DeleteNode(const std::string &id) { - return m_nodeEditorWindow.GetNodeEntryLabel(nodeId); + m_story->DeleteNode(id); +} + +void MainWindow::DeleteLink(std::shared_ptr c) +{ + m_story->DeleteLink(c); } std::list> MainWindow::GetNodeConnections(const std::string &nodeId) { - return m_nodeEditorWindow.GetNodeConnections(nodeId); + return m_story->GetNodeConnections(nodeId); } bool MainWindow::CompileToAssembler() @@ -846,7 +855,7 @@ bool MainWindow::CompileToAssembler() // FIXME // 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 if (ret) diff --git a/story-editor/src/main_window.h b/story-editor/src/main_window.h index 1dfccd7..4410a70 100644 --- a/story-editor/src/main_window.h +++ b/story-editor/src/main_window.h @@ -134,9 +134,13 @@ private: virtual void ClearResources() override; virtual std::pair Resources() override; virtual void DeleteResource(FilterIterator &it) override; + + virtual std::shared_ptr CreateNode(const std::string &type) override; virtual void Build(bool compileonly) override; + virtual void DeleteNode(const std::string &id) override; + virtual void DeleteLink(std::shared_ptr c) override; virtual std::list> GetNodeConnections(const std::string &nodeId) override; - virtual std::string GetNodeEntryLabel(const std::string &nodeId) override; + virtual void Play() override; virtual void Ok() override; virtual void Stop() override; diff --git a/story-editor/src/node_editor/base_node_widget.cpp b/story-editor/src/node_editor/base_node_widget.cpp index c36517b..54b7794 100644 --- a/story-editor/src/node_editor/base_node_widget.cpp +++ b/story-editor/src/node_editor/base_node_widget.cpp @@ -5,14 +5,18 @@ unsigned long BaseNodeWidget::s_nextId = 1; -BaseNodeWidget::BaseNodeWidget(const std::string &type, IStoryManager &proj) - : BaseNode(type) - , m_story(proj) +BaseNodeWidget::BaseNodeWidget(IStoryManager &manager, std::shared_ptr base) + : m_manager(manager) + , m_base(base) { - // m_id = UUID().String(); m_node = std::make_unique(GetNextId(), ""); // ImGui internal ID } +BaseNodeWidget::~BaseNodeWidget() +{ + std::cout << "Deleted node widget" << std::endl; +} + void BaseNodeWidget::AddInput() { m_node->Inputs.emplace_back(GetNextId(), "", PinType::Flow); @@ -60,9 +64,12 @@ void BaseNodeWidget::FrameStart() if (m_firstFrame) { - // 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(BaseNode::GetX(), BaseNode::GetY())); + if (m_base) + { + // 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; } diff --git a/story-editor/src/node_editor/base_node_widget.h b/story-editor/src/node_editor/base_node_widget.h index 54caf1c..684dcbf 100644 --- a/story-editor/src/node_editor/base_node_widget.h +++ b/story-editor/src/node_editor/base_node_widget.h @@ -95,24 +95,16 @@ struct Link * @brief Basically a wrapper class around ImGuiNodeEditor Node structure * */ -class BaseNodeWidget : public BaseNode +class BaseNodeWidget { public: - struct NodePosition - { - float x; - float y; - }; - - BaseNodeWidget(const std::string &type, IStoryManager &proj); + BaseNodeWidget(IStoryManager &manager, std::shared_ptr base); + virtual ~BaseNodeWidget(); virtual void Initialize(); virtual void Draw() = 0; virtual void DrawProperties() = 0; - virtual std::string GenerateConstants() = 0; - virtual std::string Build() = 0; - virtual std::string GetEntryLabel() = 0; void FrameStart(); @@ -201,14 +193,16 @@ public: void SetOutputs(uint32_t num); void DeleteOutput(); + std::shared_ptr Base() { return m_base; } + private: - IStoryManager &m_story; + IStoryManager &m_manager; + std::shared_ptr m_base; std::unique_ptr m_node; bool m_firstFrame{true}; static unsigned long s_nextId; - }; diff --git a/story-editor/src/node_editor/media_node.cpp b/story-editor/src/node_editor/media_node.cpp deleted file mode 100644 index 1064ced..0000000 --- a/story-editor/src/node_editor/media_node.cpp +++ /dev/null @@ -1,339 +0,0 @@ - -#include -#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()); - SetSound(j["sound"].get()); -} - -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> 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> 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(); -} - diff --git a/story-editor/src/node_editor/media_node_widget.cpp b/story-editor/src/node_editor/media_node_widget.cpp new file mode 100644 index 0000000..4e73feb --- /dev/null +++ b/story-editor/src/node_editor/media_node_widget.cpp @@ -0,0 +1,204 @@ + +#include +#include "media_node_widget.h" + +namespace ed = ax::NodeEditor; +#include "IconsMaterialDesignIcons.h" +#include "story_project.h" + +MediaNodeWidget::MediaNodeWidget(IStoryManager &manager, std::shared_ptr node) + : BaseNodeWidget(manager, node) + , m_manager(manager) +{ + m_mediaNode = std::dynamic_pointer_cast(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()); + SetSound(j["sound"].get()); +} + + +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(); +} + + diff --git a/story-editor/src/node_editor/media_node.h b/story-editor/src/node_editor/media_node_widget.h similarity index 60% rename from story-editor/src/node_editor/media_node.h rename to story-editor/src/node_editor/media_node_widget.h index 74b9243..f942a23 100644 --- a/story-editor/src/node_editor/media_node.h +++ b/story-editor/src/node_editor/media_node_widget.h @@ -7,34 +7,33 @@ #include "base_node_widget.h" #include "i_story_manager.h" +#include "i_story_project.h" #include "gui.h" #include +#include "media_node.h" - -class MediaNode : public BaseNodeWidget +class MediaNodeWidget : public BaseNodeWidget { public: - MediaNode(const std::string &title, IStoryManager &proj); + MediaNodeWidget(IStoryManager &manager, std::shared_ptr node); void Draw() override; virtual void DrawProperties() override; - virtual std::string Build() override; - virtual std::string GetEntryLabel() override; - virtual std::string GenerateConstants() override; - virtual void Initialize() override; - private: - IStoryManager &m_story; + IStoryManager &m_manager; + std::shared_ptr m_mediaNode; Gui::Image m_image; - std::string m_soundName; + std::string m_soundPath; std::string m_buttonUniqueName; + void SetImage(const std::string &f); void SetSound(const std::string &f); - std::string ChoiceLabel() const; + void StoreInternalData(); + }; diff --git a/story-editor/src/node_editor/node_editor_window.cpp b/story-editor/src/node_editor/node_editor_window.cpp index c88de56..d30a6ce 100644 --- a/story-editor/src/node_editor/node_editor_window.cpp +++ b/story-editor/src/node_editor/node_editor_window.cpp @@ -9,7 +9,7 @@ #include #include "IconsFontAwesome5_c.h" -#include "media_node.h" +#include "media_node_widget.h" #include "gui.h" #include "uuid.h" @@ -21,12 +21,12 @@ if (!(x)) { \ #include "json.hpp" -NodeEditorWindow::NodeEditorWindow(IStoryManager &proj) +NodeEditorWindow::NodeEditorWindow(IStoryManager &manager) : WindowBase("Node editor") - , m_story(proj) + , m_manager(manager) { - registerNode("media-node"); + registerNode("media-node"); } NodeEditorWindow::~NodeEditorWindow() @@ -46,21 +46,17 @@ void NodeEditorWindow::Initialize() void NodeEditorWindow::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 id = 0; for (auto & n : m_nodes) { - if (n->GetId() == modelNodeId) + if (n->Base()->GetId() == modelNodeId) { id = n->GetInputPinAt(pinIndex); break; @@ -81,7 +77,7 @@ ed::PinId NodeEditorWindow::GetOutputPin(const std::string &modelNodeId, int pin for (auto & n : m_nodes) { - if (n->GetId() == modelNodeId) + if (n->Base()->GetId() == modelNodeId) { id = n->GetOutputPinAt(pinIndex); break; @@ -96,92 +92,59 @@ ed::PinId NodeEditorWindow::GetOutputPin(const std::string &modelNodeId, int pin return id; } -void NodeEditorWindow::Load(const nlohmann::json &model) +void NodeEditorWindow::Load(std::shared_ptr story) { - try { + m_story = story; - nlohmann::json nodesJsonArray = model["nodes"]; + if (m_story) + { + try { - BaseNodeWidget::InitId(); - m_nodes.clear(); - m_links.clear(); + BaseNodeWidget::InitId(); + m_nodes.clear(); + m_links.clear(); - for (auto& element : nodesJsonArray) { - - std::string type = element["type"].get(); + auto [node_begin, node_end] = m_story->Nodes(); - auto n = createNode(type, m_story); - if (n) + for (auto it = node_begin; it != node_end; ++it) { - n->FromJson(element); - n->Initialize(); - 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 outputCounts; - - for (auto& connection : connectionJsonArray) - { - Connection model = connection.get(); - - - if (outputCounts.count(model.outNodeId) > 0) + auto n = CreateNodeWidget((*it)->GetType(), m_manager, (*it)); + if (n) { - 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 { - 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 n : m_nodes) - { - if (outputCounts.count(n->GetId()) > 0) - { - n->SetOutputs(outputCounts[n->GetId()]); - } - } - - CreateLink(model, - GetInputPin(model.inNodeId, model.inPortIndex), - GetOutputPin(model.outNodeId, model.outPortIndex)); - - + for (auto it = link_begin; it != link_end; ++it) + { + CreateLink(*it, + GetInputPin((*it)->inNodeId, (*it)->inPortIndex), + GetOutputPin((*it)->outNodeId, (*it)->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 model, ed::PinId inId, ed::PinId outId) { auto conn = std::make_shared(); - *conn->model = model; + conn->model = model; // ImGui stuff for links 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); } -void NodeEditorWindow::Save(nlohmann::json &model) + + +std::shared_ptr NodeEditorWindow::LinkToModel(ed::PinId InputId, ed::PinId OutputId) { - ed::SetCurrentEditor(m_context); - // 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; + auto c = std::make_shared(); int index; for (const auto & n : m_nodes) { if (n->HasOnputPinId(OutputId, index)) { - c.outNodeId = n->GetId(); - c.outPortIndex = index; + c->outNodeId = n->Base()->GetId(); + c->outPortIndex = index; } if (n->HasInputPinId(InputId, index)) { - c.inNodeId = n->GetId(); - c.inPortIndex = index; + c->inNodeId = n->Base()->GetId(); + c->inPortIndex = index; } } 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> NodeEditorWindow::GetNodeConnections(const std::string &nodeId) -{ - std::list> 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 NodeEditorWindow::GetSelectedNode() { @@ -417,9 +251,10 @@ void NodeEditorWindow::Draw() // ed::AcceptNewItem() return true when user release mouse button. 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. ed::Link(m_links.back()->ed_link->Id, inputPinId, outputPinId); @@ -442,9 +277,13 @@ void NodeEditorWindow::Draw() { if (ed::AcceptDeletedItem()) { - auto id = std::find_if(m_nodes.begin(), m_nodes.end(), [nodeId](std::shared_ptr node) { return node->GetInternalId() == nodeId.Get(); }); - if (id != m_nodes.end()) - m_nodes.erase(id); + auto it = std::find_if(m_nodes.begin(), m_nodes.end(), [nodeId](std::shared_ptr node) { return node->GetInternalId() == nodeId.Get(); }); + if (it != m_nodes.end()) + { + // 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()) { - m_links.erase(std::remove_if(m_links.begin(), - m_links.end(), - [deletedLinkId](std::shared_ptr inf) - { - return inf->ed_link->Id == deletedLinkId; - })); + auto it = std::find_if(m_links.begin(), m_links.end(), [deletedLinkId](std::shared_ptr inf) { return inf->ed_link->Id == deletedLinkId; }); + if (it != m_links.end()) + { + // First delete model, then current entry + m_manager.DeleteLink((*it)->model); + m_links.erase(it); + } } // You may reject link deletion by calling: @@ -486,13 +326,16 @@ void NodeEditorWindow::Draw() Node* node = nullptr; if (ImGui::MenuItem("Media Node")) { - auto n = createNode("media-node", m_story); - if (n) + auto base = m_manager.CreateNode("media-node"); + if (base) { - n->SetId(GenerateNodeId()); - n->SetPosition(newNodePostion.x, newNodePostion.y); - n->Initialize(); - m_nodes.push_back(n); + auto n = CreateNodeWidget(base->GetType(), m_manager, base); + if (n) + { + n->Base()->SetPosition(newNodePostion.x, newNodePostion.y); + n->Initialize(); + m_nodes.push_back(n); + } } } diff --git a/story-editor/src/node_editor/node_editor_window.h b/story-editor/src/node_editor/node_editor_window.h index daa021b..d3c3912 100644 --- a/story-editor/src/node_editor/node_editor_window.h +++ b/story-editor/src/node_editor/node_editor_window.h @@ -9,6 +9,7 @@ #include "window_base.h" #include "i_story_manager.h" #include "json.hpp" +#include "story_project.h" namespace ed = ax::NodeEditor; @@ -46,28 +47,24 @@ public: std::shared_ptr model; }; - NodeEditorWindow(IStoryManager &proj); + NodeEditorWindow(IStoryManager &manager); ~NodeEditorWindow(); virtual void Draw() override; void Initialize(); void Clear(); - void Load(const nlohmann::json &model); - void Save(nlohmann::json &model); - bool Build(std::string &codeStr); - std::list > GetNodeConnections(const std::string &nodeId); - std::string GetNodeEntryLabel(const std::string &nodeId); + void Load(std::shared_ptr story); std::shared_ptr GetSelectedNode(); private: - IStoryManager &m_story; - - ed::EditorContext* m_context = nullptr; + IStoryManager &m_manager; bool m_loaded{false}; - // key: Id + ed::EditorContext* m_context = nullptr; + + std::shared_ptr m_story; std::list> m_nodes; std::list> m_links; // List of live links. It is dynamic unless you want to create read-only view over nodes. void ToolbarUI(); @@ -89,12 +86,12 @@ private: template struct Factory { - static std::shared_ptr create_func(const std::string &type, IStoryManager &proj) { - return std::make_shared(type, proj); + static std::shared_ptr create_func(IStoryManager &manager, std::shared_ptr base) { + return std::make_shared(manager, base); } }; - typedef std::shared_ptr (*GenericCreator)(const std::string &type, IStoryManager &proj); + typedef std::shared_ptr (*GenericCreator)(IStoryManager &manager, std::shared_ptr base); typedef std::map Registry; Registry m_registry; @@ -103,7 +100,7 @@ private: m_registry.insert(typename Registry::value_type(key, Factory::create_func)); } - std::shared_ptr createNode(const std::string& key, IStoryManager &proj) { + std::shared_ptr CreateNodeWidget(const std::string& key, IStoryManager &manager, std::shared_ptr base) { typename Registry::const_iterator i = m_registry.find(key); if (i == m_registry.end()) { throw std::invalid_argument(std::string(__PRETTY_FUNCTION__) + @@ -111,16 +108,13 @@ private: } else { - return i->second(key, proj); + return i->second(manager, base); } } - ed::PinId GetInputPin(const std::string &modelNodeId, int pinIndex); ed::PinId GetOutputPin(const std::string &modelNodeId, int pinIndex); - std::string FindFirstNode() const; - std::string GenerateNodeId(); - void CreateLink(const Connection &model, ed::PinId inId, ed::PinId outId); - Connection LinkToModel(ed::PinId InputId, ed::PinId OutputId); + void CreateLink(std::shared_ptr model, ed::PinId inId, ed::PinId outId); + std::shared_ptr LinkToModel(ed::PinId InputId, ed::PinId OutputId); }; diff --git a/story-editor/src/node_engine/base_node.cpp b/story-editor/src/node_engine/base_node.cpp index 63b06d6..a60f1ad 100644 --- a/story-editor/src/node_engine/base_node.cpp +++ b/story-editor/src/node_engine/base_node.cpp @@ -1,12 +1,29 @@ #include "base_node.h" - +#include "uuid.h" #include BaseNode::BaseNode(const std::string &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) { try @@ -16,6 +33,7 @@ void BaseNode::FromJson(const nlohmann::json &j) m_type = j["type"].get(); m_title = j.value("title", "Default node"); nlohmann::json posJson = j["position"]; + SetPosition(posJson["x"].get(), posJson["y"].get()); } catch (std::exception& e) diff --git a/story-editor/src/node_engine/base_node.h b/story-editor/src/node_engine/base_node.h index 9b08913..56bdff5 100644 --- a/story-editor/src/node_engine/base_node.h +++ b/story-editor/src/node_engine/base_node.h @@ -2,11 +2,11 @@ #include #include -#include "json.hpp" #include #include #include "json.hpp" +#include "i_story_project.h" class BaseNode { @@ -18,6 +18,12 @@ public: }; 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); @@ -50,6 +56,6 @@ private: std::string m_uuid; NodePosition m_pos; - nlohmann::json m_internal_data; + nlohmann::json m_internal_data{{}}; }; diff --git a/story-editor/src/node_engine/i_script_node.h b/story-editor/src/node_engine/i_script_node.h new file mode 100644 index 0000000..9a6394d --- /dev/null +++ b/story-editor/src/node_engine/i_script_node.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#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; +}; diff --git a/story-editor/src/node_engine/media_node.cpp b/story-editor/src/node_engine/media_node.cpp new file mode 100644 index 0000000..169be5d --- /dev/null +++ b/story-editor/src/node_engine/media_node.cpp @@ -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> 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> 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(); +} + diff --git a/story-editor/src/node_engine/media_node.h b/story-editor/src/node_engine/media_node.h new file mode 100644 index 0000000..1bc99c2 --- /dev/null +++ b/story-editor/src/node_engine/media_node.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#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; +}; + diff --git a/story-editor/src/properties_window.cpp b/story-editor/src/properties_window.cpp index 5062e4a..cb16e65 100644 --- a/story-editor/src/properties_window.cpp +++ b/story-editor/src/properties_window.cpp @@ -25,7 +25,7 @@ void PropertiesWindow::Draw() if (m_selectedNode) { 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(); } diff --git a/story-editor/src/window_base.h b/story-editor/src/window_base.h index a2974cc..dca828f 100644 --- a/story-editor/src/window_base.h +++ b/story-editor/src/window_base.h @@ -7,6 +7,7 @@ class WindowBase { public: WindowBase(const std::string &title); + virtual ~WindowBase() {} bool IsDisabled() const { return m_disabled;