From c594e019122344dec8f69992f803f6e9b549c4a8 Mon Sep 17 00:00:00 2001 From: Anthony Rabine Date: Fri, 24 Oct 2025 21:00:15 +0200 Subject: [PATCH] new function entry/exit nodes with parameters --- .../interfaces/i_story_manager.h | 3 + core/story-manager/src/nodes/base_node.h | 32 +++ .../src/nodes/call_function_node.h | 176 +++++++++++++++- .../src/nodes/function_entry_node.h | 102 +++++++++- .../src/nodes/function_exit_node.h | 123 ++++++++++++ core/story-manager/src/nodes_factory.h | 33 +-- core/story-manager/src/story_project.cpp | 36 +++- core/story-manager/src/story_project.h | 4 +- .../story-manager/src/utils}/json.hpp | 0 core/story-manager/src/utils/json_wrapper.cpp | 186 +++++++++++++++++ core/story-manager/src/utils/json_wrapper.h | 82 ++++++++ core/tests/CMakeLists.txt | 3 + core/tests/test_json.cpp | 101 ++++++++++ story-editor/CMakeLists.txt | 3 +- story-editor/imgui.ini | 94 +++++---- story-editor/src/app/app_controller.cpp | 5 + story-editor/src/app/app_controller.h | 2 +- story-editor/src/main_window.cpp | 8 +- .../node_editor/call_function_node_widget.h | 189 +++++++++++++++++- .../node_editor/function_entry_node_widget.h | 136 +++++++++++++ .../src/node_editor/function_entry_widget.h | 45 ----- .../node_editor/function_exit_node_widget.h | 143 +++++++++++++ 22 files changed, 1366 insertions(+), 140 deletions(-) create mode 100644 core/story-manager/src/nodes/function_exit_node.h rename {shared => core/story-manager/src/utils}/json.hpp (100%) create mode 100644 core/story-manager/src/utils/json_wrapper.cpp create mode 100644 core/story-manager/src/utils/json_wrapper.h create mode 100644 core/tests/test_json.cpp create mode 100644 story-editor/src/node_editor/function_entry_node_widget.h delete mode 100644 story-editor/src/node_editor/function_entry_widget.h create mode 100644 story-editor/src/node_editor/function_exit_node_widget.h diff --git a/core/story-manager/interfaces/i_story_manager.h b/core/story-manager/interfaces/i_story_manager.h index ed6247e..5b061a2 100644 --- a/core/story-manager/interfaces/i_story_manager.h +++ b/core/story-manager/interfaces/i_story_manager.h @@ -27,6 +27,8 @@ struct Callback { template std::function Callback::func; +class NodesFactory; + class IStoryManager { public: @@ -50,6 +52,7 @@ public: virtual void LoadBinaryStory(const std::string &filename) = 0; virtual void ToggleBreakpoint(int line) = 0; virtual uint32_t GetRegister(int reg) = 0; + virtual NodesFactory& GetNodesFactory() = 0; virtual void Play() = 0; virtual void Step() = 0; diff --git a/core/story-manager/src/nodes/base_node.h b/core/story-manager/src/nodes/base_node.h index dc90e7a..d7bb466 100644 --- a/core/story-manager/src/nodes/base_node.h +++ b/core/story-manager/src/nodes/base_node.h @@ -136,6 +136,38 @@ public: m_outputPorts.clear(); } + // Clear only data input ports, keep execution ports + void ClearDataInputPorts() { + auto it = m_inputPorts.begin(); + while (it != m_inputPorts.end()) { + if (it->type == Port::Type::DATA_PORT) { + it = m_inputPorts.erase(it); + } else { + ++it; + } + } + } + + // Clear only data output ports, keep execution ports + void ClearDataOutputPorts() { + auto it = m_outputPorts.begin(); + while (it != m_outputPorts.end()) { + if (it->type == Port::Type::DATA_PORT) { + it = m_outputPorts.erase(it); + } else { + ++it; + } + } + } + + void ClearAllInputPorts() { + m_inputPorts.clear(); + } + + void ClearAllOutputPorts() { + m_outputPorts.clear(); + } + // Port management void AddInputPort(Port::Type type, const std::string& label, bool customRendering = false) { m_inputPorts.push_back({type, label, customRendering}); diff --git a/core/story-manager/src/nodes/call_function_node.h b/core/story-manager/src/nodes/call_function_node.h index 989f86d..7ba56d2 100644 --- a/core/story-manager/src/nodes/call_function_node.h +++ b/core/story-manager/src/nodes/call_function_node.h @@ -2,28 +2,92 @@ #pragma once #include "base_node.h" +#include class CallFunctionNode : public BaseNode { public: + enum InputBindingMode { + MODE_CONNECTED, // Value comes from a connected pin + MODE_CONSTANT // Value is a constant set in the UI + }; + + struct InputBinding { + std::string paramName; + InputBindingMode mode; + std::string constantValue; + + nlohmann::json ToJson() const { + return { + {"paramName", paramName}, + {"mode", mode == MODE_CONNECTED ? "connected" : "constant"}, + {"constantValue", constantValue} + }; + } + + static InputBinding FromJson(const nlohmann::json& j) { + InputBinding ib; + ib.paramName = j.value("paramName", ""); + std::string modeStr = j.value("mode", "connected"); + ib.mode = (modeStr == "constant") ? MODE_CONSTANT : MODE_CONNECTED; + ib.constantValue = j.value("constantValue", ""); + return ib; + } + }; + + struct OutputMapping { + std::string returnValueName; + std::string targetVariable; // Optional: map to a global variable + + nlohmann::json ToJson() const { + return { + {"returnValueName", returnValueName}, + {"targetVariable", targetVariable} + }; + } + + static OutputMapping FromJson(const nlohmann::json& j) { + OutputMapping om; + om.returnValueName = j.value("returnValueName", ""); + om.targetVariable = j.value("targetVariable", ""); + return om; + } + }; + CallFunctionNode(const std::string &type) : BaseNode(type, "Call Function Node") , m_functionName("") , m_functionUuid("") { SetBehavior(BaseNode::BEHAVIOR_EXECUTION); - SetupExecutionPorts(true, 1, true); // 1 entrée, 1 sortie + SetupExecutionPorts(true, 1, true); // 1 entrée, sorties dynamiques } void Initialize() override { - // Charger le nom et l'UUID de la fonction depuis les données internes nlohmann::json j = GetInternalData(); + if (j.contains("functionName")) { m_functionName = j["functionName"].get(); } if (j.contains("functionUuid")) { m_functionUuid = j["functionUuid"].get(); } + + // Load input bindings + m_inputBindings.clear(); + if (j.contains("inputBindings") && j["inputBindings"].is_array()) { + for (const auto& ibJson : j["inputBindings"]) { + m_inputBindings.push_back(InputBinding::FromJson(ibJson)); + } + } + + // Load output mappings + m_outputMappings.clear(); + if (j.contains("outputMappings") && j["outputMappings"].is_array()) { + for (const auto& omJson : j["outputMappings"]) { + m_outputMappings.push_back(OutputMapping::FromJson(omJson)); + } + } } std::string GetFunctionName() const { @@ -37,15 +101,113 @@ public: void SetFunction(const std::string& uuid, const std::string& name) { m_functionUuid = uuid; m_functionName = name; + SaveData(); + } + + // Input bindings management + const std::vector& GetInputBindings() const { + return m_inputBindings; + } + + void SetInputBindingMode(const std::string& paramName, InputBindingMode mode, const std::string& constantValue = "") { + // Find or create binding + auto it = std::find_if(m_inputBindings.begin(), m_inputBindings.end(), + [¶mName](const InputBinding& ib) { return ib.paramName == paramName; }); - // Sauvegarder dans les données internes pour la persistance - nlohmann::json j; - j["functionName"] = m_functionName; - j["functionUuid"] = m_functionUuid; - SetInternalData(j); + if (it != m_inputBindings.end()) { + it->mode = mode; + it->constantValue = constantValue; + } else { + InputBinding ib; + ib.paramName = paramName; + ib.mode = mode; + ib.constantValue = constantValue; + m_inputBindings.push_back(ib); + } + + SaveData(); + } + + InputBinding* GetInputBinding(const std::string& paramName) { + auto it = std::find_if(m_inputBindings.begin(), m_inputBindings.end(), + [¶mName](const InputBinding& ib) { return ib.paramName == paramName; }); + + return (it != m_inputBindings.end()) ? &(*it) : nullptr; + } + + // Output mappings management + const std::vector& GetOutputMappings() const { + return m_outputMappings; + } + + void SetOutputMapping(const std::string& returnValueName, const std::string& targetVariable) { + auto it = std::find_if(m_outputMappings.begin(), m_outputMappings.end(), + [&returnValueName](const OutputMapping& om) { return om.returnValueName == returnValueName; }); + + if (it != m_outputMappings.end()) { + it->targetVariable = targetVariable; + } else { + OutputMapping om; + om.returnValueName = returnValueName; + om.targetVariable = targetVariable; + m_outputMappings.push_back(om); + } + + SaveData(); + } + + // Rebuild ports based on the module's interface + void RebuildPortsFromModule(const std::vector>& parameters, + const std::vector>& exitLabels, + const std::map>>& returnValuesByExit) { + // Clear all ports except the main execution input + ClearDataInputPorts(); + ClearAllOutputPorts(); + + // Add data input ports for each parameter + for (const auto& param : parameters) { + AddInputPort(Port::Type::DATA_PORT, param.first, false); + } + + // Add execution output ports for each exit + for (const auto& exitLabel : exitLabels) { + AddOutputPort(Port::Type::EXECUTION_PORT, exitLabel.first, true); + } + + // Add data output ports for all unique return values across all exits + std::set allReturnValues; + for (const auto& exitPair : returnValuesByExit) { + for (const auto& rv : exitPair.second) { + allReturnValues.insert(rv.first); + } + } + + for (const auto& rvName : allReturnValues) { + AddOutputPort(Port::Type::DATA_PORT, rvName, false); + } } private: std::string m_functionName; std::string m_functionUuid; + std::vector m_inputBindings; + std::vector m_outputMappings; + + void SaveData() { + nlohmann::json j; + j["functionName"] = m_functionName; + j["functionUuid"] = m_functionUuid; + + j["inputBindings"] = nlohmann::json::array(); + for (const auto& ib : m_inputBindings) { + j["inputBindings"].push_back(ib.ToJson()); + } + + j["outputMappings"] = nlohmann::json::array(); + for (const auto& om : m_outputMappings) { + j["outputMappings"].push_back(om.ToJson()); + } + + SetInternalData(j); + } }; \ No newline at end of file diff --git a/core/story-manager/src/nodes/function_entry_node.h b/core/story-manager/src/nodes/function_entry_node.h index 2676104..0304021 100644 --- a/core/story-manager/src/nodes/function_entry_node.h +++ b/core/story-manager/src/nodes/function_entry_node.h @@ -1,11 +1,33 @@ #pragma once #include "base_node.h" - +#include class FunctionEntryNode : public BaseNode { public: + struct Parameter { + std::string name; + std::string type; // "int", "string", "bool", "float" + std::string defaultValue; + + nlohmann::json ToJson() const { + return { + {"name", name}, + {"type", type}, + {"defaultValue", defaultValue} + }; + } + + static Parameter FromJson(const nlohmann::json& j) { + Parameter p; + p.name = j.value("name", ""); + p.type = j.value("type", "int"); + p.defaultValue = j.value("defaultValue", ""); + return p; + } + }; + FunctionEntryNode(const std::string &type) : BaseNode(type, "Function Entry Node") { @@ -15,12 +37,78 @@ public: } void Initialize() override { - // Initialisation spécifique pour FunctionEntryNode - // Par exemple, préparer les entrées nécessaires pour la fonction + // Load parameters from internal data + nlohmann::json j = GetInternalData(); + m_parameters.clear(); + + if (j.contains("parameters") && j["parameters"].is_array()) { + for (const auto& paramJson : j["parameters"]) { + m_parameters.push_back(Parameter::FromJson(paramJson)); + } + } + + // Rebuild output ports for parameters + RebuildParameterPorts(); } - // Ajoutez des méthodes spécifiques pour gérer l'entrée de la fonction - void PrepareFunctionEntry() { - // Logique pour préparer l'entrée de la fonction + void AddParameter(const std::string& name, const std::string& type, const std::string& defaultValue = "") { + Parameter param; + param.name = name; + param.type = type; + param.defaultValue = defaultValue; + m_parameters.push_back(param); + + SaveParameters(); + RebuildParameterPorts(); } -}; + + void RemoveParameter(size_t index) { + if (index < m_parameters.size()) { + m_parameters.erase(m_parameters.begin() + index); + SaveParameters(); + RebuildParameterPorts(); + } + } + + void UpdateParameter(size_t index, const std::string& name, const std::string& type, const std::string& defaultValue) { + if (index < m_parameters.size()) { + m_parameters[index].name = name; + m_parameters[index].type = type; + m_parameters[index].defaultValue = defaultValue; + SaveParameters(); + RebuildParameterPorts(); + } + } + + const std::vector& GetParameters() const { + return m_parameters; + } + + size_t GetParameterCount() const { + return m_parameters.size(); + } + +private: + std::vector m_parameters; + + void SaveParameters() { + nlohmann::json j; + j["parameters"] = nlohmann::json::array(); + + for (const auto& param : m_parameters) { + j["parameters"].push_back(param.ToJson()); + } + + SetInternalData(j); + } + + void RebuildParameterPorts() { + // Clear all data output ports (keep execution port) + ClearDataOutputPorts(); + + // Add a data output port for each parameter + for (const auto& param : m_parameters) { + AddOutputPort(Port::Type::DATA_PORT, param.name, false); + } + } +}; \ No newline at end of file diff --git a/core/story-manager/src/nodes/function_exit_node.h b/core/story-manager/src/nodes/function_exit_node.h new file mode 100644 index 0000000..276e3cf --- /dev/null +++ b/core/story-manager/src/nodes/function_exit_node.h @@ -0,0 +1,123 @@ +#pragma once + +#include "base_node.h" +#include + +class FunctionExitNode : public BaseNode +{ +public: + struct ReturnValue { + std::string name; + std::string type; // "int", "string", "bool", "float" + + nlohmann::json ToJson() const { + return { + {"name", name}, + {"type", type} + }; + } + + static ReturnValue FromJson(const nlohmann::json& j) { + ReturnValue rv; + rv.name = j.value("name", ""); + rv.type = j.value("type", "int"); + return rv; + } + }; + + FunctionExitNode(const std::string &type) + : BaseNode(type, "Function Exit Node") + , m_exitLabel("Return") + { + SetWeight(900); // High weight, near the end + SetBehavior(BaseNode::BEHAVIOR_EXECUTION); + SetupExecutionPorts(true, 0, true); // Has input, no output (it's the end) + } + + void Initialize() override { + // Load return values and exit label from internal data + nlohmann::json j = GetInternalData(); + m_returnValues.clear(); + + m_exitLabel = j.value("exitLabel", "Return"); + + if (j.contains("returnValues") && j["returnValues"].is_array()) { + for (const auto& rvJson : j["returnValues"]) { + m_returnValues.push_back(ReturnValue::FromJson(rvJson)); + } + } + + // Rebuild input ports for return values + RebuildReturnValuePorts(); + } + + void SetExitLabel(const std::string& label) { + m_exitLabel = label; + SaveData(); + } + + std::string GetExitLabel() const { + return m_exitLabel; + } + + void AddReturnValue(const std::string& name, const std::string& type) { + ReturnValue rv; + rv.name = name; + rv.type = type; + m_returnValues.push_back(rv); + + SaveData(); + RebuildReturnValuePorts(); + } + + void RemoveReturnValue(size_t index) { + if (index < m_returnValues.size()) { + m_returnValues.erase(m_returnValues.begin() + index); + SaveData(); + RebuildReturnValuePorts(); + } + } + + void UpdateReturnValue(size_t index, const std::string& name, const std::string& type) { + if (index < m_returnValues.size()) { + m_returnValues[index].name = name; + m_returnValues[index].type = type; + SaveData(); + RebuildReturnValuePorts(); + } + } + + const std::vector& GetReturnValues() const { + return m_returnValues; + } + + size_t GetReturnValueCount() const { + return m_returnValues.size(); + } + +private: + std::string m_exitLabel; + std::vector m_returnValues; + + void SaveData() { + nlohmann::json j; + j["exitLabel"] = m_exitLabel; + j["returnValues"] = nlohmann::json::array(); + + for (const auto& rv : m_returnValues) { + j["returnValues"].push_back(rv.ToJson()); + } + + SetInternalData(j); + } + + void RebuildReturnValuePorts() { + // Clear all data input ports (keep execution port) + ClearDataInputPorts(); + + // Add a data input port for each return value + for (const auto& rv : m_returnValues) { + AddInputPort(Port::Type::DATA_PORT, rv.name, false); + } + } +}; \ No newline at end of file diff --git a/core/story-manager/src/nodes_factory.h b/core/story-manager/src/nodes_factory.h index 9f1dce2..21f2244 100644 --- a/core/story-manager/src/nodes_factory.h +++ b/core/story-manager/src/nodes_factory.h @@ -24,21 +24,24 @@ #include "while_loop_node.h" #include "break_node.h" #include "continue_node.h" +#include "function_exit_node.h" -static const std::string OperatorNodeUuid = "0226fdac-8f7a-47d7-8584-b23aceb712ec"; -static const std::string CallFunctionNodeUuid = "02745f38-9b11-49fe-94b1-b2a6b78249fb"; -static const std::string VariableNodeUuid = "020cca4e-9cdc-47e7-a6a5-53e4c9152ed0"; -static const std::string PrintNodeUuid = "02ee27bc-ff1d-4f94-b700-eab55052ad1c"; -static const std::string FunctionEntryNodeUuid = "02fd145a-b3a6-43c2-83ce-6a187e6d4b5b"; -static const std::string BranchNodeUuid = "027b723d-2327-4646-a17a-79ddc2e016e4"; -static const std::string WaitEventNodeUuid = "02225cff-4975-400e-8130-41524d8af773"; -static const std::string WaitDelayNodeUuid = "02455ef0-4975-4546-94de-720cae6baae3"; -static const std::string PlayMediaNodeUuid = "0285e90a-2eb7-4605-baa9-b3712a14dff8"; -static const std::string SendSignalNodeUuid = "02c2ce4b-8783-47cb-a55f-90056bebd64b"; -static const std::string ForLoopNodeUuid = "02a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"; -static const std::string WhileLoopNodeUuid = "02b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"; -static const std::string BreakNodeUuid = "02c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e"; -static const std::string ContinueNodeUuid = "02d4e5f6-7a8b-9c0d-1e2f-3a4b5c6d7e8f"; + +static constexpr const char* OperatorNodeUuid = "0226fdac-8f7a-47d7-8584-b23aceb712ec"; +static constexpr const char* CallFunctionNodeUuid = "02745f38-9b11-49fe-94b1-b2a6b78249fb"; +static constexpr const char* VariableNodeUuid = "020cca4e-9cdc-47e7-a6a5-53e4c9152ed0"; +static constexpr const char* PrintNodeUuid = "02ee27bc-ff1d-4f94-b700-eab55052ad1c"; +static constexpr const char* FunctionEntryNodeUuid = "02fd145a-b3a6-43c2-83ce-6a187e6d4b5b"; +static constexpr const char* BranchNodeUuid = "027b723d-2327-4646-a17a-79ddc2e016e4"; +static constexpr const char* WaitEventNodeUuid = "02225cff-4975-400e-8130-41524d8af773"; +static constexpr const char* WaitDelayNodeUuid = "02455ef0-4975-4546-94de-720cae6baae3"; +static constexpr const char* PlayMediaNodeUuid = "0285e90a-2eb7-4605-baa9-b3712a14dff8"; +static constexpr const char* SendSignalNodeUuid = "02c2ce4b-8783-47cb-a55f-90056bebd64b"; +static constexpr const char* ForLoopNodeUuid = "02a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"; +static constexpr const char* WhileLoopNodeUuid = "02b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"; +static constexpr const char* BreakNodeUuid = "02c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e"; +static constexpr const char* ContinueNodeUuid = "02d4e5f6-7a8b-9c0d-1e2f-3a4b5c6d7e8f"; +static constexpr const char* FunctionExitNodeUuid = "02d78b65-9246-4108-91fc-03dfc142d9ee"; typedef std::shared_ptr (*GenericCreator)(const std::string &type); @@ -66,6 +69,8 @@ public: registerNode(WhileLoopNodeUuid, std::make_shared("While Loop")); registerNode(BreakNodeUuid, std::make_shared("Break")); registerNode(ContinueNodeUuid, std::make_shared("Continue")); + registerNode(FunctionExitNodeUuid, std::make_shared("Function exit")); + } ~NodesFactory() = default; diff --git a/core/story-manager/src/story_project.cpp b/core/story-manager/src/story_project.cpp index 5fff668..ee55ca5 100644 --- a/core/story-manager/src/story_project.cpp +++ b/core/story-manager/src/story_project.cpp @@ -7,7 +7,6 @@ #include "story_project.h" #include "json.hpp" -// #include "media_node.h" #include "variable_node.h" #include "operator_node.h" #include "print_node.h" @@ -234,6 +233,18 @@ void StoryProject::ScanVariable(const std::function)>& callback) { + for (auto& page : m_pages) { + auto [nodesBegin, nodesEnd] = page->Nodes(); + std::vector> pageNodes(nodesBegin, nodesEnd); + for (auto& node : pageNodes) { + if (!callback(node)) { + return; // Stop scanning if callback returns false + } + } + } +} + void StoryProject::AddVariable() { auto v = std::make_shared("var_" + std::to_string(m_variables.size())); @@ -271,6 +282,8 @@ bool StoryProject::ModelFromJson(const nlohmann::json &model, NodesFactory &fact std::string type = element["type"].get(); + std::cout << "!!!!!!!!!!!!!!!!!" << type << std::endl; + auto n = factory.CreateNode(type); if (n) { @@ -301,16 +314,13 @@ bool StoryProject::ModelFromJson(const nlohmann::json &model, NodesFactory &fact p->AddLink(std::make_shared(connection.get())); } } - - } - success = true; } catch(nlohmann::json::exception &e) { - std::cout << "(NodeEditorWindow::Load) " << e.what() << std::endl; + std::cout << "(StoryProject::ModelFromJson) " << e.what() << std::endl; } return success; @@ -533,8 +543,14 @@ bool StoryProject::Load(ResourceManager &manager, NodesFactory &factory) if (j.contains("pages")) { - ModelFromJson(j, factory); - m_initialized = true; + if (ModelFromJson(j, factory)) + { + m_initialized = true; + } + else + { + throw std::logic_error("Model read error"); + } } if (j.contains("variables")) @@ -575,7 +591,11 @@ bool StoryProject::Load(ResourceManager &manager, NodesFactory &factory) } catch(nlohmann::json::exception &e) { - std::cout << e.what() << std::endl; + m_log.Log(e.what(), true); + } + catch(const std::exception &e) + { + m_log.Log(e.what(), true); } if (m_pages.size() == 0) diff --git a/core/story-manager/src/story_project.h b/core/story-manager/src/story_project.h index 1a908cc..bbf10c5 100644 --- a/core/story-manager/src/story_project.h +++ b/core/story-manager/src/story_project.h @@ -5,7 +5,7 @@ #include #include #include -#include "json.hpp" + #include "json.hpp" #include "resource_manager.h" @@ -103,7 +103,7 @@ public: std::pair>::iterator, std::list>::iterator> Nodes(const std::string_view &page_uuid); std::pair>::iterator, std::list>::iterator> Links(const std::string_view &page_uuid); - + void ScanNodes(const std::function)>& callback); void ScanVariable(const std::function element)>& operation) override; void AddVariable() override; void DeleteVariable(int i) override; diff --git a/shared/json.hpp b/core/story-manager/src/utils/json.hpp similarity index 100% rename from shared/json.hpp rename to core/story-manager/src/utils/json.hpp diff --git a/core/story-manager/src/utils/json_wrapper.cpp b/core/story-manager/src/utils/json_wrapper.cpp new file mode 100644 index 0000000..57f56ca --- /dev/null +++ b/core/story-manager/src/utils/json_wrapper.cpp @@ -0,0 +1,186 @@ +#include "json_wrapper.h" +#include "json.hpp" +#include +#include +#include +#include + + +using json = nlohmann::json; + +class JsonWrapper::Impl { +public: + json j; +}; + +JsonWrapper::JsonWrapper() : impl(std::make_unique()) { + impl->j = json::object(); +} + +JsonWrapper::JsonWrapper(JsonWrapper&& other) noexcept : impl(std::move(other.impl)) {} + +JsonWrapper& JsonWrapper::operator=(JsonWrapper&& other) noexcept { + if (this != &other) { + impl = std::move(other.impl); + } + return *this; +} + +JsonWrapper::JsonWrapper(const std::string& jsonString) : impl(std::make_unique()) { + try { + impl->j = json::parse(jsonString); + } catch (...) { + impl->j = json::object(); + } +} + +JsonWrapper::~JsonWrapper() = default; + +bool JsonWrapper::hasKey(const std::string& key) const { + return impl->j.contains(key); +} + +std::optional JsonWrapper::getString(const std::string& key, std::string& errorKey) const { + try { + return impl->j.at(key).get(); + } catch (...) { + errorKey = key; + return std::nullopt; + } +} + +std::optional JsonWrapper::getInt(const std::string& key, std::string& errorKey) const { + try { + return impl->j.at(key).get(); + } catch (...) { + errorKey = key; + return std::nullopt; + } +} + +std::optional JsonWrapper::getDouble(const std::string& key, std::string& errorKey) const { + try { + return impl->j.at(key).get(); + } catch (...) { + errorKey = key; + return std::nullopt; + } +} + +std::optional JsonWrapper::getBool(const std::string& key, std::string& errorKey) const { + try { + return impl->j.at(key).get(); + } catch (...) { + errorKey = key; + return std::nullopt; + } +} + +void JsonWrapper::setString(const std::string& key, const std::string& value) { + impl->j[key] = value; +} + +void JsonWrapper::setInt(const std::string& key, int value) { + impl->j[key] = value; +} + +void JsonWrapper::setDouble(const std::string& key, double value) { + impl->j[key] = value; +} + +void JsonWrapper::setBool(const std::string& key, bool value) { + impl->j[key] = value; +} + +void JsonWrapper::setArrayImpl(const std::string& key, const void* values, size_t typeHash) { + // Ici on doit caster et stocker dans json, mais sans RTTI avancé on limite + // Par exemple on peut specialiser cette méthode pour les types courants + if (typeHash == typeid(int).hash_code()) { + const auto& v = *reinterpret_cast*>(values); + impl->j[key] = v; + } else if (typeHash == typeid(double).hash_code()) { + const auto& v = *reinterpret_cast*>(values); + impl->j[key] = v; + } else if (typeHash == typeid(std::string).hash_code()) { + const auto& v = *reinterpret_cast*>(values); + impl->j[key] = v; + } else if (typeHash == typeid(bool).hash_code()) { + const auto& v = *reinterpret_cast*>(values); + impl->j[key] = v; + } else { + // Pour les types non pris en charge, on peut lever une exception ou ignorer + throw std::runtime_error("Type non supporté pour setArray"); + } +} + +template +std::optional> JsonWrapper::getArrayImpl(const std::string& key, std::string& errorKey) const { + try { + return impl->j.at(key).get>(); + } catch (...) { + errorKey = key; + return std::nullopt; + } +} + +template +T JsonWrapper::getImpl(const std::string& key, std::string& errorKey) const { + try { + return impl->j.at(key).get(); + } catch (...) { + errorKey = key; + throw; + } +} + +template +T JsonWrapper::asImpl(std::string& errorKey) const { + try { + return impl->j.get(); + } catch (...) { + errorKey = "[root]"; + throw; + } +} + +JsonWrapper JsonWrapper::fromImpl(const void* value, size_t typeHash) { + JsonWrapper wrapper; + if (typeHash == typeid(int).hash_code()) { + wrapper.impl->j = *reinterpret_cast(value); + } else if (typeHash == typeid(double).hash_code()) { + wrapper.impl->j = *reinterpret_cast(value); + } else if (typeHash == typeid(std::string).hash_code()) { + wrapper.impl->j = *reinterpret_cast(value); + } else if (typeHash == typeid(bool).hash_code()) { + wrapper.impl->j = *reinterpret_cast(value); + } else { + // Si c’est un type complexe, il faut une surcharge ou un specialization dans le .cpp + // Exemple: si T a to_json/from_json, on peut faire une conversion nlohmann::json value = T; + // Ici on fait un cast "générique" (moins safe) + // On peut faire un throw ici pour forcer l’utilisateur à spécialiser. + throw std::runtime_error("Type non supporté pour from()"); + } + return std::move(wrapper); +} + +// Explicit instantiations for getArrayImpl, getImpl, asImpl for common types + +template std::optional> JsonWrapper::getArrayImpl(const std::string&, std::string&) const; +template std::optional> JsonWrapper::getArrayImpl(const std::string&, std::string&) const; +template std::optional> JsonWrapper::getArrayImpl(const std::string&, std::string&) const; +template std::optional> JsonWrapper::getArrayImpl(const std::string&, std::string&) const; + +template int JsonWrapper::getImpl(const std::string&, std::string&) const; +template double JsonWrapper::getImpl(const std::string&, std::string&) const; +template std::string JsonWrapper::getImpl(const std::string&, std::string&) const; +template bool JsonWrapper::getImpl(const std::string&, std::string&) const; + +template int JsonWrapper::asImpl(std::string&) const; +template double JsonWrapper::asImpl(std::string&) const; +template std::string JsonWrapper::asImpl(std::string&) const; +template bool JsonWrapper::asImpl(std::string&) const; + +std::string JsonWrapper::dump(int indent) const { + if (indent < 0) return impl->j.dump(); + return impl->j.dump(indent); +} diff --git a/core/story-manager/src/utils/json_wrapper.h b/core/story-manager/src/utils/json_wrapper.h new file mode 100644 index 0000000..b3e145f --- /dev/null +++ b/core/story-manager/src/utils/json_wrapper.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include + +class JsonWrapper { +public: + JsonWrapper(); + explicit JsonWrapper(const std::string& jsonString); + ~JsonWrapper(); + + // Ajout move constructor et move assign + JsonWrapper(JsonWrapper&&) noexcept; + JsonWrapper& operator=(JsonWrapper&&) noexcept; + + // Supprimer copie pour éviter erreur + JsonWrapper(const JsonWrapper&) = delete; + JsonWrapper& operator=(const JsonWrapper&) = delete; + + bool hasKey(const std::string& key) const; + + // Valeurs simples + std::optional getString(const std::string& key, std::string& errorKey) const; + std::optional getInt(const std::string& key, std::string& errorKey) const; + std::optional getDouble(const std::string& key, std::string& errorKey) const; + std::optional getBool(const std::string& key, std::string& errorKey) const; + + void setString(const std::string& key, const std::string& value); + void setInt(const std::string& key, int value); + void setDouble(const std::string& key, double value); + void setBool(const std::string& key, bool value); + + // Array génériques + template + void setArray(const std::string& key, const std::vector& values) { + setArrayImpl(key, reinterpret_cast(&values), typeid(T).hash_code()); + } + + template + std::optional> getArray(const std::string& key, std::string& errorKey) const { + return getArrayImpl(key, errorKey); + } + + // Sérialisation/Désérialisation générique + template + T get(const std::string& key, std::string& errorKey) const { + return getImpl(key, errorKey); + } + + template + T as(std::string& errorKey) const { + return asImpl(errorKey); + } + + template + static JsonWrapper from(const T& value) { + return fromImpl(reinterpret_cast(&value), typeid(T).hash_code()); + } + + // Dump JSON + std::string dump(int indent = -1) const; + +private: + class Impl; + std::unique_ptr impl; + + // Implémentations privées non template (définies dans .cpp) + void setArrayImpl(const std::string& key, const void* values, size_t typeHash); + + template + std::optional> getArrayImpl(const std::string& key, std::string& errorKey) const; + + template + T getImpl(const std::string& key, std::string& errorKey) const; + + template + T asImpl(std::string& errorKey) const; + + static JsonWrapper fromImpl(const void* value, size_t typeHash); +}; diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index 3e85b47..4777f57 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(${PROJECT_NAME} test_print_node.cpp test_branch.cpp test_loops.cpp + test_json.cpp ../story-manager/src/nodes/base_node.cpp ../story-manager/src/nodes/branch_node.cpp @@ -47,6 +48,7 @@ add_executable(${PROJECT_NAME} ../story-manager/src/nodes/break_node.cpp ../story-manager/src/nodes/continue_node.cpp + ../story-manager/src/utils/json_wrapper.cpp ../chip32/chip32_assembler.cpp ../chip32/chip32_vm.c @@ -58,6 +60,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE ../story-manager/src ../story-manager/src/nodes ../story-manager/src/compiler + ../story-manager/src/utils ../story-manager/interfaces ../../shared ) diff --git a/core/tests/test_json.cpp b/core/tests/test_json.cpp new file mode 100644 index 0000000..45ddbe9 --- /dev/null +++ b/core/tests/test_json.cpp @@ -0,0 +1,101 @@ +#include +#include +#include "json_wrapper.h" +#include "json.hpp" +using Catch::Matchers::WithinRel; +using namespace nlohmann; + +TEST_CASE("JsonWrapper basic types", "[json]") { + JsonWrapper json; + json.setString("name", "Alice"); + json.setInt("age", 30); + json.setDouble("pi", 3.14159); + json.setBool("is_valid", true); + + std::string err; + + SECTION("Get string") { + auto name = json.getString("name", err); + REQUIRE(name.has_value()); + REQUIRE(name.value() == "Alice"); + } + + SECTION("Get int") { + auto age = json.getInt("age", err); + REQUIRE(age.has_value()); + REQUIRE(age.value() == 30); + } + + SECTION("Get double") { + auto pi = json.getDouble("pi", err); + REQUIRE(pi.has_value()); + REQUIRE_THAT(pi.value(), WithinRel(3.14159, 1e-6)); + } + + SECTION("Get bool") { + auto valid = json.getBool("is_valid", err); + REQUIRE(valid.has_value()); + REQUIRE(valid.value() == true); + } +} + +TEST_CASE("JsonWrapper arrays", "[json]") { + JsonWrapper json; + + json.setArray("scores", {10, 20, 30}); + json.setArray("tags", {"news", "tech"}); + json.setArray("measurements", {1.1, 2.2, 3.3}); + json.setArray("flags", {true, false, true}); + + std::string err; + + auto scores = json.getArray("scores", err); + REQUIRE(scores.has_value()); + REQUIRE(scores->size() == 3); + REQUIRE(scores->at(1) == 20); + + auto tags = json.getArray("tags", err); + REQUIRE(tags.has_value()); + REQUIRE(tags->at(0) == "news"); + + auto measurements = json.getArray("measurements", err); + REQUIRE(measurements.has_value()); + REQUIRE_THAT(measurements->at(2), WithinRel(3.3, 1e-6)); + + auto flags = json.getArray("flags", err); + REQUIRE(flags.has_value()); + REQUIRE(flags->at(0) == true); +} + +struct Connection { + std::string host; + int port;ccM9XAGZ$mz^b*52T5p&sMA@ujPbCUNW +}; + +// from_json / to_json must be defined for Connection +inline void to_json(nlohmann::json& j, const Connection& c) { + j = nlohmann::json{{"host", c.host}, {"port", c.port}}; +} + +inline void from_json(const nlohmann::json& j, Connection& c) { + j.at("host").get_to(c.host); + j.at("port").get_to(c.port); +} + +template Connection JsonWrapper::asImpl(std::string&) const; +template Connection JsonWrapper::getImpl(const std::string&, std::string&) const; +template std::optional> JsonWrapper::getArrayImpl(const std::string&, std::string&) const; + + +TEST_CASE("JsonWrapper generic serialization/deserialization", "[json][struct]") { + + + Connection original{"127.0.0.1", 5000}; + auto wrapper = JsonWrapper::from(original); + + std::string err; + auto restored = wrapper.as(err); + + REQUIRE(restored.host == "127.0.0.1"); + REQUIRE(restored.port == 5000); +} diff --git a/story-editor/CMakeLists.txt b/story-editor/CMakeLists.txt index 60ca59b..5c262a7 100644 --- a/story-editor/CMakeLists.txt +++ b/story-editor/CMakeLists.txt @@ -292,6 +292,7 @@ target_include_directories(${STORY_EDITOR_PROJECT} PUBLIC ../core/story-manager/src ../core/story-manager/src/nodes ../core/story-manager/src/compiler + ../core/story-manager/src/utils ../core/story-manager/interfaces ) @@ -300,7 +301,7 @@ add_definitions( -DVERSION_MAJOR=${PROJECT_VERSION_MAJOR} -DVERSION_MINOR=${PROJECT_VERSION_MINOR} -DVERSION_PATCH=${PROJECT_VERSION_PATCH} - + -DJSON_DIAGNOSTICS=1 -DRAUDIO_STANDALONE -DSUPPORT_MODULE_RAUDIO ) diff --git a/story-editor/imgui.ini b/story-editor/imgui.ini index b42a55d..aec3560 100644 --- a/story-editor/imgui.ini +++ b/story-editor/imgui.ini @@ -1,6 +1,6 @@ [Window][WindowOverViewport_11111111] Pos=60,26 -Size=1860,982 +Size=1220,694 Collapsed=0 [Window][Debug##Default] @@ -9,34 +9,34 @@ Size=400,400 Collapsed=0 [Window][Library Manager] -Pos=1060,26 -Size=860,653 +Pos=730,26 +Size=550,441 Collapsed=0 DockId=0x00000002,0 [Window][Console] -Pos=60,681 -Size=805,327 +Pos=60,469 +Size=527,251 Collapsed=0 DockId=0x00000004,0 [Window][Emulator] -Pos=1060,26 -Size=860,653 -Collapsed=0 -DockId=0x00000002,5 - -[Window][Code viewer] -Pos=1060,26 -Size=860,653 +Pos=730,26 +Size=550,441 Collapsed=0 DockId=0x00000002,4 -[Window][Resources] -Pos=1060,26 -Size=860,653 +[Window][Code viewer] +Pos=730,26 +Size=550,441 Collapsed=0 -DockId=0x00000002,1 +DockId=0x00000002,3 + +[Window][Resources] +Pos=589,469 +Size=691,251 +Collapsed=0 +DockId=0x00000005,1 [Window][Node editor] Pos=60,26 @@ -50,36 +50,36 @@ Size=150,42 Collapsed=0 [Window][Variables] -Pos=867,681 -Size=1053,327 +Pos=589,469 +Size=691,251 Collapsed=0 DockId=0x00000005,0 [Window][CPU] -Pos=1060,26 -Size=860,653 +Pos=730,26 +Size=550,441 +Collapsed=0 +DockId=0x00000002,1 + +[Window][RAM view] +Pos=730,26 +Size=550,441 Collapsed=0 DockId=0x00000002,2 -[Window][RAM view] -Pos=1060,26 -Size=860,653 -Collapsed=0 -DockId=0x00000002,3 - [Window][Properties] -Pos=867,681 -Size=1053,327 +Pos=730,26 +Size=550,441 Collapsed=0 -DockId=0x00000005,1 +DockId=0x00000002,5 [Window][ToolBar] Pos=0,26 -Size=60,982 +Size=60,694 Collapsed=0 [Window][QuitConfirm] -Pos=828,456 +Pos=508,312 Size=264,96 Collapsed=0 @@ -90,13 +90,13 @@ Collapsed=0 [Window][Module editor] Pos=60,26 -Size=998,653 +Size=668,441 Collapsed=0 DockId=0x00000001,0 [Window][Story editor] Pos=60,26 -Size=998,653 +Size=668,441 Collapsed=0 DockId=0x00000001,1 @@ -106,11 +106,21 @@ Size=687,422 Collapsed=0 [Window][Error List] -Pos=60,681 -Size=805,327 +Pos=60,469 +Size=527,251 Collapsed=0 DockId=0x00000004,1 +[Window][AboutPopup] +Pos=436,235 +Size=408,249 +Collapsed=0 + +[Window][ModulePropertiesPopup] +Pos=424,289 +Size=432,142 +Collapsed=0 + [Table][0x7728942D,5] RefScale=20 Column 0 Width=44 Sort=0v @@ -155,11 +165,11 @@ Column 2 Weight=1.0000 Column 3 Width=60 [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=60,26 Size=1860,982 Split=Y - DockNode ID=0x00000007 Parent=0x08BD597D SizeRef=1220,365 Split=X - DockNode ID=0x00000001 Parent=0x00000007 SizeRef=998,694 CentralNode=1 Selected=0x93ADCAAB - DockNode ID=0x00000002 Parent=0x00000007 SizeRef=860,694 Selected=0x4B07C626 - DockNode ID=0x00000008 Parent=0x08BD597D SizeRef=1220,327 Split=X Selected=0xEA83D666 +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=60,26 Size=1220,694 Split=Y + DockNode ID=0x00000007 Parent=0x08BD597D SizeRef=1220,441 Split=X + DockNode ID=0x00000001 Parent=0x00000007 SizeRef=668,694 CentralNode=1 Selected=0x93ADCAAB + DockNode ID=0x00000002 Parent=0x00000007 SizeRef=550,694 Selected=0x63869CAF + DockNode ID=0x00000008 Parent=0x08BD597D SizeRef=1220,251 Split=X Selected=0xEA83D666 DockNode ID=0x00000004 Parent=0x00000008 SizeRef=621,192 Selected=0xEA83D666 - DockNode ID=0x00000005 Parent=0x00000008 SizeRef=813,192 Selected=0x8C72BEA8 + DockNode ID=0x00000005 Parent=0x00000008 SizeRef=813,192 Selected=0x30401527 diff --git a/story-editor/src/app/app_controller.cpp b/story-editor/src/app/app_controller.cpp index 30e6ef1..6699991 100644 --- a/story-editor/src/app/app_controller.cpp +++ b/story-editor/src/app/app_controller.cpp @@ -852,6 +852,11 @@ void AppController::ImportProject(const std::string &filePathName, int format) } +NodesFactory& AppController::GetNodesFactory() +{ + return m_nodesFactory; +} + std::shared_ptr AppController::GetCurrentProject() { return m_story; diff --git a/story-editor/src/app/app_controller.h b/story-editor/src/app/app_controller.h index 93ad3f6..baaec05 100644 --- a/story-editor/src/app/app_controller.h +++ b/story-editor/src/app/app_controller.h @@ -77,6 +77,7 @@ public: virtual void LoadBinaryStory(const std::string &filename) override; virtual void ToggleBreakpoint(int line) override; virtual uint32_t GetRegister(int reg) override; + virtual NodesFactory& GetNodesFactory() override; virtual void Play() override; virtual void Step() override; virtual void Run() override; @@ -113,7 +114,6 @@ public: // Getters pour les managers gérés par AppController ResourceManager& GetResourceManager() { return m_resources; } LibraryManager& GetLibraryManager() { return m_libraryManager; } - NodesFactory& GetNodesFactory() { return m_nodesFactory; } AudioPlayer& GetAudioPlayer() { return m_player; } WebServer& GetWebServer() { return m_webServer; } const std::vector& GetRecentProjects() const { return m_recentProjects; } diff --git a/story-editor/src/main_window.cpp b/story-editor/src/main_window.cpp index 837cdfe..f6427fb 100644 --- a/story-editor/src/main_window.cpp +++ b/story-editor/src/main_window.cpp @@ -35,7 +35,7 @@ #include "variable_node_widget.h" #include "operator_node_widget.h" #include "print_node_widget.h" -#include "function_entry_widget.h" +#include "function_entry_node_widget.h" #include "branch_node_widget.h" #include "play_media_node_widget.h" #include "send_signal_node_widget.h" @@ -45,6 +45,8 @@ #include "while_loop_node_widget.h" #include "break_node_widget.h" #include "continue_node_widget.h" +#include "function_exit_node_widget.h" + MainWindow::MainWindow(ILogger& logger, EventBus& eventBus, AppController& appController) : m_logger(logger) @@ -74,7 +76,7 @@ MainWindow::MainWindow(ILogger& logger, EventBus& eventBus, AppController& appCo // m_widgetFactory.registerNode("module-node"); m_widgetFactory.registerNode(VariableNodeUuid); m_widgetFactory.registerNode(PrintNodeUuid); - m_widgetFactory.registerNode(FunctionEntryNodeUuid); + m_widgetFactory.registerNode(FunctionEntryNodeUuid); m_widgetFactory.registerNode(BranchNodeUuid); m_widgetFactory.registerNode(WaitEventNodeUuid); m_widgetFactory.registerNode(WaitDelayNodeUuid); @@ -84,6 +86,8 @@ MainWindow::MainWindow(ILogger& logger, EventBus& eventBus, AppController& appCo m_widgetFactory.registerNode(WhileLoopNodeUuid); m_widgetFactory.registerNode(BreakNodeUuid); m_widgetFactory.registerNode(ContinueNodeUuid); + m_widgetFactory.registerNode(FunctionExitNodeUuid); + m_eventBus.Subscribe([this](const OpenProjectEvent &event) { OpenProject(event.GetUuid()); diff --git a/story-editor/src/node_editor/call_function_node_widget.h b/story-editor/src/node_editor/call_function_node_widget.h index 334705a..11600e3 100644 --- a/story-editor/src/node_editor/call_function_node_widget.h +++ b/story-editor/src/node_editor/call_function_node_widget.h @@ -9,7 +9,10 @@ #include "i_story_manager.h" #include "i_story_project.h" #include "call_function_node.h" +#include "function_entry_node.h" +#include "function_exit_node.h" #include "gui.h" +#include "IconsMaterialDesignIcons.h" class CallFunctionNodeWidget : public BaseNodeWidget { @@ -26,16 +29,25 @@ public: BaseNodeWidget::Initialize(); m_functionName = m_callFunctionNode->GetFunctionName(); m_functionUuid = m_callFunctionNode->GetFunctionUuid(); + + // If a function is selected, rebuild ports + if (!m_functionUuid.empty()) { + RebuildPortsFromModule(); + } } void DrawProperties(std::shared_ptr story) override { - ImGui::AlignTextToFramePadding(); - - // Liste déroulante des fonctions disponibles - if (ImGui::BeginCombo("Function", m_functionName.empty() ? "" : m_functionName.c_str())) { + // Get list of available functions/modules + auto functions = story->GetFunctionsList(); for (size_t i = 0; i < functions.size(); ++i) { const bool is_selected = (m_functionUuid == functions[i].uuid); @@ -44,6 +56,9 @@ public: m_functionUuid = functions[i].uuid; m_functionName = functions[i].name; m_callFunctionNode->SetFunction(m_functionUuid, m_functionName); + + // Rebuild ports based on the selected module + RebuildPortsFromModule(); } if (is_selected) @@ -52,25 +67,38 @@ public: ImGui::EndCombo(); } - // Bouton pour ouvrir la fonction dans l'éditeur ImGui::Spacing(); - - // Désactiver le bouton si aucune fonction n'est sélectionnée + + // Open function button if (m_functionUuid.empty()) { ImGui::BeginDisabled(); } - if (ImGui::Button("> Open function")) { + if (ImGui::Button(ICON_MDI_OPEN_IN_NEW " Open function")) { m_manager.OpenFunction(m_functionUuid, m_functionName); } if (m_functionUuid.empty()) { ImGui::EndDisabled(); } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // If a function is selected, show input/output configuration + if (!m_functionUuid.empty() && !m_moduleParameters.empty()) { + DrawInputBindingsUI(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + DrawOutputMappingsUI(story); + } } void Draw() override { - // Afficher le nom de la fonction dans le noeud ImGui::TextUnformatted(m_functionName.empty() ? "" : m_functionName.c_str()); @@ -81,4 +109,143 @@ private: std::shared_ptr m_callFunctionNode; std::string m_functionName; std::string m_functionUuid; + + // Cache of module interface + std::vector> m_moduleParameters; // name, type + std::vector m_exitLabels; + std::map>> m_returnValuesByExit; + + void RebuildPortsFromModule() { + // Get the module from NodesFactory + auto module = m_manager.GetNodesFactory().GetModule(m_functionUuid); + if (!module) { + return; + } + + // Find FunctionEntryNode in the module + m_moduleParameters.clear(); + std::shared_ptr entryNode = nullptr; + + module->ScanNodes([&](std::shared_ptr node) { + if (node->GetType() == "function-entry-node") { + entryNode = std::dynamic_pointer_cast(node); + return false; + } + return true; + }); + + if (entryNode) { + for (const auto& param : entryNode->GetParameters()) { + m_moduleParameters.push_back({param.name, param.type}); + } + } + + // Find all FunctionExitNodes in the module + m_exitLabels.clear(); + m_returnValuesByExit.clear(); + std::vector> exitNodes; + + module->ScanNodes([&](std::shared_ptr node) { + if (node->GetType() == "function-exit-node") { + auto exitNode = std::dynamic_pointer_cast(node); + if (exitNode) { + exitNodes.push_back(exitNode); + m_exitLabels.push_back(exitNode->GetExitLabel()); + + std::vector> returnValues; + for (const auto& rv : exitNode->GetReturnValues()) { + returnValues.push_back({rv.name, rv.type}); + } + m_returnValuesByExit[exitNode->GetExitLabel()] = returnValues; + } + } + return true; + }); + + // Rebuild the node's ports + std::vector> exitLabelsWithType; + for (const auto& label : m_exitLabels) { + exitLabelsWithType.push_back({label, "execution"}); + } + + m_callFunctionNode->RebuildPortsFromModule(m_moduleParameters, exitLabelsWithType, m_returnValuesByExit); + } + + void DrawInputBindingsUI() { + ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), ICON_MDI_ARROW_RIGHT " Input Parameters"); + ImGui::Spacing(); + ImGui::Text("Configure how each parameter receives its value:"); + ImGui::Spacing(); + + if (ImGui::BeginTable("input_bindings_table", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("Constant Value", ImGuiTableColumnFlags_WidthFixed, 150.0f); + ImGui::TableHeadersRow(); + + for (const auto& param : m_moduleParameters) { + ImGui::TableNextRow(); + ImGui::PushID(param.first.c_str()); + + // Parameter name + ImGui::TableNextColumn(); + ImGui::Text("%s (%s)", param.first.c_str(), param.second.c_str()); + + // Mode selection + ImGui::TableNextColumn(); + auto binding = m_callFunctionNode->GetInputBinding(param.first); + int currentMode = (binding && binding->mode == CallFunctionNode::MODE_CONSTANT) ? 1 : 0; + + const char* modes[] = {"Connected", "Constant"}; + if (ImGui::Combo("##mode", ¤tMode, modes, 2)) { + auto mode = (currentMode == 1) ? CallFunctionNode::MODE_CONSTANT : CallFunctionNode::MODE_CONNECTED; + std::string value = binding ? binding->constantValue : ""; + m_callFunctionNode->SetInputBindingMode(param.first, mode, value); + } + + // Constant value (only if mode is constant) + ImGui::TableNextColumn(); + if (currentMode == 1) { + char valueBuf[256] = ""; + if (binding) { + strncpy(valueBuf, binding->constantValue.c_str(), sizeof(valueBuf) - 1); + } + + if (ImGui::InputText("##value", valueBuf, sizeof(valueBuf))) { + m_callFunctionNode->SetInputBindingMode(param.first, CallFunctionNode::MODE_CONSTANT, valueBuf); + } + } else { + ImGui::TextDisabled("(from pin)"); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MDI_INFORMATION_OUTLINE " Use 'Connected' to get values from connected pins, 'Constant' to set a fixed value."); + } + + void DrawOutputMappingsUI(std::shared_ptr story) { + ImGui::TextColored(ImVec4(0.8f, 0.4f, 0.2f, 1.0f), ICON_MDI_ARROW_LEFT " Return Values"); + ImGui::Spacing(); + + // Show all return values grouped by exit + for (const auto& exitPair : m_returnValuesByExit) { + ImGui::Text("Exit: %s", exitPair.first.c_str()); + ImGui::Indent(); + + for (const auto& rv : exitPair.second) { + ImGui::BulletText("%s (%s)", rv.first.c_str(), rv.second.c_str()); + } + + ImGui::Unindent(); + ImGui::Spacing(); + } + + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MDI_INFORMATION_OUTLINE " Return values are available on output pins."); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Connect them to use the returned data."); + } }; \ No newline at end of file diff --git a/story-editor/src/node_editor/function_entry_node_widget.h b/story-editor/src/node_editor/function_entry_node_widget.h new file mode 100644 index 0000000..d59c622 --- /dev/null +++ b/story-editor/src/node_editor/function_entry_node_widget.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include +#include + +#include "base_node_widget.h" +#include "i_story_manager.h" +#include "i_story_project.h" +#include "function_entry_node.h" +#include "gui.h" +#include "IconsMaterialDesignIcons.h" + +class FunctionEntryNodeWidget : public BaseNodeWidget +{ +public: + FunctionEntryNodeWidget(IStoryManager &manager, std::shared_ptr node) + : BaseNodeWidget(manager, node) + , m_manager(manager) + { + m_functionEntryNode = std::dynamic_pointer_cast(node); + SetTitle("Function Entry"); + } + + void Draw() override { + ImGui::TextUnformatted("Entry Point"); + if (m_functionEntryNode->GetParameterCount() > 0) { + ImGui::Text(ICON_MDI_ARROW_RIGHT " %zu parameter(s)", m_functionEntryNode->GetParameterCount()); + } + } + + virtual bool HasSync() const override { + return false; + } + + virtual void DrawProperties(std::shared_ptr story) override { + ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), ICON_MDI_IMPORT " Function Entry"); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("This node defines the entry point of the function/module."); + ImGui::Text("Add parameters that will be passed when this function is called."); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Parameters list + ImGui::Text(ICON_MDI_FORMAT_LIST_BULLETED " Parameters:"); + ImGui::Spacing(); + + const auto& parameters = m_functionEntryNode->GetParameters(); + + // Table for parameters + if (ImGui::BeginTable("parameters_table", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Default", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableHeadersRow(); + + // Display existing parameters + for (size_t i = 0; i < parameters.size(); ++i) { + ImGui::TableNextRow(); + ImGui::PushID(static_cast(i)); + + // Name + ImGui::TableNextColumn(); + char nameBuf[128]; + strncpy(nameBuf, parameters[i].name.c_str(), sizeof(nameBuf) - 1); + nameBuf[sizeof(nameBuf) - 1] = '\0'; + + if (ImGui::InputText("##name", nameBuf, sizeof(nameBuf))) { + m_functionEntryNode->UpdateParameter(i, nameBuf, parameters[i].type, parameters[i].defaultValue); + } + + // Type + ImGui::TableNextColumn(); + const char* types[] = {"int", "float", "string", "bool"}; + int currentType = 0; + for (int t = 0; t < 4; ++t) { + if (parameters[i].type == types[t]) { + currentType = t; + break; + } + } + + if (ImGui::Combo("##type", ¤tType, types, 4)) { + m_functionEntryNode->UpdateParameter(i, parameters[i].name, types[currentType], parameters[i].defaultValue); + } + + // Default value + ImGui::TableNextColumn(); + char defaultBuf[128]; + strncpy(defaultBuf, parameters[i].defaultValue.c_str(), sizeof(defaultBuf) - 1); + defaultBuf[sizeof(defaultBuf) - 1] = '\0'; + + if (ImGui::InputText("##default", defaultBuf, sizeof(defaultBuf))) { + m_functionEntryNode->UpdateParameter(i, parameters[i].name, parameters[i].type, defaultBuf); + } + + // Actions + ImGui::TableNextColumn(); + if (ImGui::Button(ICON_MDI_DELETE "##delete")) { + m_functionEntryNode->RemoveParameter(i); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::Spacing(); + + // Add new parameter + if (ImGui::Button(ICON_MDI_PLUS " Add Parameter")) { + m_functionEntryNode->AddParameter("newParam", "int", "0"); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Info + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MDI_INFORMATION_OUTLINE " Each parameter creates an output port on this node."); + } + + virtual void Initialize() override { + BaseNodeWidget::Initialize(); + } + +private: + IStoryManager &m_manager; + std::shared_ptr m_functionEntryNode; +}; diff --git a/story-editor/src/node_editor/function_entry_widget.h b/story-editor/src/node_editor/function_entry_widget.h deleted file mode 100644 index 8b23b15..0000000 --- a/story-editor/src/node_editor/function_entry_widget.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "base_node_widget.h" -#include "i_story_manager.h" -#include "i_story_project.h" -#include "function_entry_node.h" -#include "gui.h" - -class FunctionEntryWidget : public BaseNodeWidget -{ -public: - FunctionEntryWidget(IStoryManager &manager, std::shared_ptr node) - : BaseNodeWidget(manager, node) - , m_manager(manager) - { - m_functionEntryNode = std::dynamic_pointer_cast(node); - SetTitle("Function Entry"); - } - - void Draw() override { - ImGui::SetNextItemWidth(100.f); - } - - virtual bool HasSync() const override { - return false; - } - - virtual void DrawProperties(std::shared_ptr story) override { - - } - virtual void Initialize() override { - - } - -private: - IStoryManager &m_manager; - - std::shared_ptr m_functionEntryNode; - -}; diff --git a/story-editor/src/node_editor/function_exit_node_widget.h b/story-editor/src/node_editor/function_exit_node_widget.h new file mode 100644 index 0000000..57a1ac0 --- /dev/null +++ b/story-editor/src/node_editor/function_exit_node_widget.h @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include +#include + +#include "base_node_widget.h" +#include "i_story_manager.h" +#include "i_story_project.h" +#include "function_exit_node.h" +#include "gui.h" +#include "IconsMaterialDesignIcons.h" + +class FunctionExitNodeWidget : public BaseNodeWidget +{ +public: + FunctionExitNodeWidget(IStoryManager &manager, std::shared_ptr node) + : BaseNodeWidget(manager, node) + , m_manager(manager) + { + m_functionExitNode = std::dynamic_pointer_cast(node); + SetTitle("Function Exit"); + } + + void Draw() override { + ImGui::TextUnformatted(m_functionExitNode->GetExitLabel().c_str()); + if (m_functionExitNode->GetReturnValueCount() > 0) { + ImGui::Text(ICON_MDI_ARROW_LEFT " %zu return value(s)", m_functionExitNode->GetReturnValueCount()); + } + } + + virtual bool HasSync() const override { + return false; + } + + virtual void DrawProperties(std::shared_ptr story) override { + ImGui::TextColored(ImVec4(0.8f, 0.2f, 0.2f, 1.0f), ICON_MDI_EXPORT " Function Exit"); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("This node defines an exit point for the function/module."); + ImGui::Text("It returns values and terminates the function execution."); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Exit label + ImGui::Text("Exit Label:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(200.0f); + + char labelBuf[128]; + strncpy(labelBuf, m_functionExitNode->GetExitLabel().c_str(), sizeof(labelBuf) - 1); + labelBuf[sizeof(labelBuf) - 1] = '\0'; + + if (ImGui::InputTextWithHint("##exit_label", "Success / Error / Cancel...", labelBuf, sizeof(labelBuf))) { + m_functionExitNode->SetExitLabel(labelBuf); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Return values list + ImGui::Text(ICON_MDI_ARROW_LEFT_BOLD " Return Values:"); + ImGui::Spacing(); + + const auto& returnValues = m_functionExitNode->GetReturnValues(); + + // Table for return values + if (ImGui::BeginTable("return_values_table", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 150.0f); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableHeadersRow(); + + // Display existing return values + for (size_t i = 0; i < returnValues.size(); ++i) { + ImGui::TableNextRow(); + ImGui::PushID(static_cast(i)); + + // Name + ImGui::TableNextColumn(); + char nameBuf[128]; + strncpy(nameBuf, returnValues[i].name.c_str(), sizeof(nameBuf) - 1); + nameBuf[sizeof(nameBuf) - 1] = '\0'; + + if (ImGui::InputText("##name", nameBuf, sizeof(nameBuf))) { + m_functionExitNode->UpdateReturnValue(i, nameBuf, returnValues[i].type); + } + + // Type + ImGui::TableNextColumn(); + const char* types[] = {"int", "float", "string", "bool"}; + int currentType = 0; + for (int t = 0; t < 4; ++t) { + if (returnValues[i].type == types[t]) { + currentType = t; + break; + } + } + + if (ImGui::Combo("##type", ¤tType, types, 4)) { + m_functionExitNode->UpdateReturnValue(i, returnValues[i].name, types[currentType]); + } + + // Actions + ImGui::TableNextColumn(); + if (ImGui::Button(ICON_MDI_DELETE "##delete")) { + m_functionExitNode->RemoveReturnValue(i); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::Spacing(); + + // Add new return value + if (ImGui::Button(ICON_MDI_PLUS " Add Return Value")) { + m_functionExitNode->AddReturnValue("result", "int"); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Info + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MDI_INFORMATION_OUTLINE " Each return value creates an input port on this node."); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "The exit label will be the name of the execution output port on CallFunctionNode."); + } + + virtual void Initialize() override { + BaseNodeWidget::Initialize(); + } + +private: + IStoryManager &m_manager; + std::shared_ptr m_functionExitNode; +}; \ No newline at end of file