aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/Model/Workflow
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2022-06-30 21:38:53 -0700
committerrtk0c <[email protected]>2022-06-30 21:38:53 -0700
commit7fe47a9d5b1727a61dc724523b530762f6d6ba19 (patch)
treee95be6e66db504ed06d00b72c579565bab873277 /app/source/Cplt/Model/Workflow
parent2cf952088d375ac8b2f45b144462af0953436cff (diff)
Restructure project
Diffstat (limited to 'app/source/Cplt/Model/Workflow')
-rw-r--r--app/source/Cplt/Model/Workflow/Evaluation.cpp174
-rw-r--r--app/source/Cplt/Model/Workflow/Evaluation.hpp67
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp18
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp13
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp94
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp44
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp231
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp53
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp32
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp23
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/fwd.hpp15
-rw-r--r--app/source/Cplt/Model/Workflow/Value.hpp94
-rw-r--r--app/source/Cplt/Model/Workflow/ValueInternals.hpp21
-rw-r--r--app/source/Cplt/Model/Workflow/Value_Main.cpp35
-rw-r--r--app/source/Cplt/Model/Workflow/Value_RTTI.cpp174
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Basic.cpp111
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Basic.hpp67
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Database.cpp88
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Database.hpp51
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Dictionary.cpp49
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Dictionary.hpp25
-rw-r--r--app/source/Cplt/Model/Workflow/Values/List.cpp100
-rw-r--r--app/source/Cplt/Model/Workflow/Values/List.hpp50
-rw-r--r--app/source/Cplt/Model/Workflow/Values/fwd.hpp17
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow.hpp316
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow_Main.cpp846
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp143
-rw-r--r--app/source/Cplt/Model/Workflow/fwd.hpp22
28 files changed, 2973 insertions, 0 deletions
diff --git a/app/source/Cplt/Model/Workflow/Evaluation.cpp b/app/source/Cplt/Model/Workflow/Evaluation.cpp
new file mode 100644
index 0000000..7035bf9
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Evaluation.cpp
@@ -0,0 +1,174 @@
+#include "Evaluation.hpp"
+
+#include <queue>
+
+const char* WorkflowEvaluationError::FormatMessageType(enum MessageType messageType)
+{
+ switch (messageType) {
+ case Error: return "Error";
+ case Warning: return "Warning";
+ }
+}
+
+const char* WorkflowEvaluationError::FormatPinType(enum PinType pinType)
+{
+ switch (pinType) {
+ case NoPin: return nullptr;
+ case InputPin: return "Input pin";
+ case OutputPin: return "Output pin";
+ }
+}
+
+std::string WorkflowEvaluationError::Format() const
+{
+ // TODO convert to std::format
+
+ std::string result;
+ result += FormatMessageType(this->Type);
+ result += " at ";
+ result += NodeId;
+ if (auto pinText = FormatPinType(this->PinType)) {
+ result += "/";
+ result += pinText;
+ result += " ";
+ result += PinId;
+ }
+ result += ": ";
+ result += this->Message;
+
+ return result;
+}
+
+struct WorkflowEvaluationContext::RuntimeNode
+{
+ enum EvaluationStatus
+ {
+ ST_Unevaluated,
+ ST_Success,
+ ST_Failed,
+ };
+
+ EvaluationStatus Status = ST_Unevaluated;
+};
+
+struct WorkflowEvaluationContext::RuntimeConnection
+{
+ std::unique_ptr<BaseValue> Value;
+
+ bool IsAvailableValue() const
+ {
+ return Value != nullptr;
+ }
+};
+
+WorkflowEvaluationContext::WorkflowEvaluationContext(Workflow& workflow)
+ : mWorkflow{ &workflow }
+{
+ mRuntimeNodes.resize(workflow.mNodes.size());
+ mRuntimeConnections.resize(workflow.mConnections.size());
+}
+
+BaseValue* WorkflowEvaluationContext::GetConnectionValue(size_t id, bool constant)
+{
+ if (constant) {
+ return mWorkflow->GetConstantById(id);
+ } else {
+ return mRuntimeConnections[id].Value.get();
+ }
+}
+
+BaseValue* WorkflowEvaluationContext::GetConnectionValue(const WorkflowNode::InputPin& inputPin)
+{
+ if (inputPin.IsConnected()) {
+ return GetConnectionValue(inputPin.Connection, inputPin.IsConstantConnection());
+ } else {
+ return nullptr;
+ }
+}
+
+void WorkflowEvaluationContext::SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value)
+{
+ mRuntimeConnections[id].Value = std::move(value);
+}
+
+void WorkflowEvaluationContext::SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value)
+{
+ if (outputPin.IsConnected()) {
+ SetConnectionValue(outputPin.Connection, std::move(value));
+ }
+}
+
+void WorkflowEvaluationContext::Run()
+{
+ int evaluatedCount = 0;
+ int erroredCount = 0;
+
+ for (auto& depthGroup : mWorkflow->GetDepthGroups()) {
+ for (size_t idx : depthGroup) {
+ auto& rn = mRuntimeNodes[idx];
+ auto& n = *mWorkflow->mNodes[idx];
+
+ // TODO
+
+ int preEvalErrors = mErrors.size();
+ n.Evaluate(*this);
+ if (preEvalErrors != mErrors.size()) {
+ erroredCount++;
+ } else {
+ evaluatedCount++;
+ }
+ }
+ }
+
+ for (size_t i = 0; i < mRuntimeNodes.size(); ++i) {
+ auto& rn = mRuntimeNodes[i];
+ auto& n = *mWorkflow->mNodes[i];
+ if (n.GetType() == WorkflowNode::OutputType) {
+ // TODO record outputs
+ }
+ }
+}
+
+void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = pinId,
+ .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin,
+ .Type = WorkflowEvaluationError::Error,
+ });
+}
+
+void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = -1,
+ .PinType = WorkflowEvaluationError::NoPin,
+ .Type = WorkflowEvaluationError::Error,
+ });
+}
+
+void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = pinId,
+ .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin,
+ .Type = WorkflowEvaluationError::Warning,
+ });
+}
+
+void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = -1,
+ .PinType = WorkflowEvaluationError::NoPin,
+ .Type = WorkflowEvaluationError::Warning,
+ });
+}
diff --git a/app/source/Cplt/Model/Workflow/Evaluation.hpp b/app/source/Cplt/Model/Workflow/Evaluation.hpp
new file mode 100644
index 0000000..5b8c6cc
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Evaluation.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+class WorkflowEvaluationError
+{
+public:
+ enum MessageType : int16_t
+ {
+ Error,
+ Warning,
+ };
+
+ enum PinType : int16_t
+ {
+ NoPin,
+ InputPin,
+ OutputPin,
+ };
+
+public:
+ std::string Message;
+ size_t NodeId;
+ int PinId;
+ PinType PinType;
+ MessageType Type;
+
+public:
+ static const char* FormatMessageType(enum MessageType messageType);
+ static const char* FormatPinType(enum PinType pinType);
+
+ std::string Format() const;
+};
+
+class WorkflowEvaluationContext
+{
+private:
+ struct RuntimeNode;
+ struct RuntimeConnection;
+
+ Workflow* mWorkflow;
+ std::vector<RuntimeNode> mRuntimeNodes;
+ std::vector<RuntimeConnection> mRuntimeConnections;
+ std::vector<WorkflowEvaluationError> mErrors;
+ std::vector<WorkflowEvaluationError> mWarnings;
+
+public:
+ WorkflowEvaluationContext(Workflow& workflow);
+
+ BaseValue* GetConnectionValue(size_t id, bool constant);
+ BaseValue* GetConnectionValue(const WorkflowNode::InputPin& inputPin);
+ void SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value);
+ void SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value);
+
+ void ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin);
+ void ReportError(std::string message, const WorkflowNode& node);
+ void ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin);
+ void ReportWarning(std::string message, const WorkflowNode& node);
+
+ /// Run until all possible paths have been evaluated.
+ void Run();
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp
new file mode 100644
index 0000000..df4a8bb
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp
@@ -0,0 +1,18 @@
+#include "DocumentNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+
+bool DocumentTemplateExpansionNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_DocumentTemplateExpansion;
+}
+
+DocumentTemplateExpansionNode::DocumentTemplateExpansionNode()
+ : WorkflowNode(KD_DocumentTemplateExpansion, false)
+{
+}
+
+void DocumentTemplateExpansionNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp
new file mode 100644
index 0000000..a266b2c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+class DocumentTemplateExpansionNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ DocumentTemplateExpansionNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp
new file mode 100644
index 0000000..f8b29bb
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp
@@ -0,0 +1,94 @@
+#include "NumericNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/Macros.hpp>
+#include <Cplt/Utils/RTTI.hpp>
+
+#include <cassert>
+#include <utility>
+
+WorkflowNode::Kind NumericOperationNode::OperationTypeToNodeKind(OperationType type)
+{
+ switch (type) {
+ case Addition: return KD_NumericAddition;
+ case Subtraction: return KD_NumericSubtraction;
+ case Multiplication: return KD_NumericMultiplication;
+ case Division: return KD_NumericDivision;
+ default: return InvalidKind;
+ }
+}
+
+NumericOperationNode::OperationType NumericOperationNode::NodeKindToOperationType(Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition: return Addition;
+ case KD_NumericSubtraction: return Subtraction;
+ case KD_NumericMultiplication: return Multiplication;
+ case KD_NumericDivision: return Division;
+ default: return InvalidType;
+ }
+}
+
+bool NumericOperationNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() >= KD_NumericAddition && node->GetKind() <= KD_NumericDivision;
+}
+
+NumericOperationNode::NumericOperationNode(OperationType type)
+ : WorkflowNode(OperationTypeToNodeKind(type), false)
+ , mType{ type }
+{
+ mInputs.resize(2);
+ mInputs[0].MatchingType = BaseValue::KD_Numeric;
+ mInputs[1].MatchingType = BaseValue::KD_Numeric;
+
+ mOutputs.resize(1);
+ mOutputs[0].MatchingType = BaseValue::KD_Numeric;
+}
+
+void NumericOperationNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+ auto lhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[0]));
+ if (!lhsVal) return;
+ double lhs = lhsVal->GetValue();
+
+ auto rhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[1]));
+ if (!rhsVal) return;
+ double rhs = rhsVal->GetValue();
+
+ double res;
+ switch (mType) {
+ case Addition: res = lhs + rhs; break;
+ case Subtraction: res = lhs - rhs; break;
+ case Multiplication: res = lhs * rhs; break;
+ case Division: {
+ if (rhs == 0.0) {
+ ctx.ReportError(I18N_TEXT("Error: division by 0", L10N_WORKFLOW_RTERROR_DIV_BY_0), *this);
+ return;
+ }
+ res = lhs / rhs;
+ } break;
+
+ default: return;
+ }
+
+ auto value = std::make_unique<NumericValue>();
+ value->SetValue(res);
+ ctx.SetConnectionValue(mOutputs[0], std::move(value));
+}
+
+bool NumericExpressionNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_NumericExpression;
+}
+
+NumericExpressionNode::NumericExpressionNode()
+ : WorkflowNode(KD_NumericExpression, false)
+{
+}
+
+void NumericExpressionNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp
new file mode 100644
index 0000000..3c89708
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <cstddef>
+#include <memory>
+#include <variant>
+#include <vector>
+
+class NumericOperationNode : public WorkflowNode
+{
+public:
+ enum OperationType
+ {
+ Addition,
+ Subtraction,
+ Multiplication,
+ Division,
+
+ InvalidType,
+ TypeCount = InvalidType,
+ };
+
+private:
+ OperationType mType;
+
+public:
+ static Kind OperationTypeToNodeKind(OperationType type);
+ static OperationType NodeKindToOperationType(Kind kind);
+ static bool IsInstance(const WorkflowNode* node);
+ NumericOperationNode(OperationType type);
+
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
+
+class NumericExpressionNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ NumericExpressionNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+}; \ No newline at end of file
diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp
new file mode 100644
index 0000000..9b31f7a
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp
@@ -0,0 +1,231 @@
+#include "TextNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/Utils/Macros.hpp>
+#include <Cplt/Utils/RTTI.hpp>
+#include <Cplt/Utils/Variant.hpp>
+
+#include <cassert>
+#include <utility>
+#include <variant>
+#include <vector>
+
+class TextFormatterNode::Impl
+{
+public:
+ template <class TFunction>
+ static void ForArguments(std::vector<Element>::iterator begin, std::vector<Element>::iterator end, const TFunction& func)
+ {
+ for (auto it = begin; it != end; ++it) {
+ auto& elm = *it;
+ if (auto arg = std::get_if<Argument>(&elm)) {
+ func(*arg);
+ }
+ }
+ }
+
+ /// Find the pin index that the \c elmIdx -th element should have, based on the elements coming before it.
+ static int FindPinForElement(const std::vector<Element>& vec, int elmIdx)
+ {
+ for (int i = elmIdx; i >= 0; --i) {
+ auto& elm = vec[i];
+ if (auto arg = std::get_if<Argument>(&elm)) {
+ return arg->PinIdx + 1;
+ }
+ }
+ return 0;
+ }
+};
+
+BaseValue::Kind TextFormatterNode::ArgumentTypeToValueKind(TextFormatterNode::ArgumentType arg)
+{
+ switch (arg) {
+ case NumericArgument: return BaseValue::KD_Numeric;
+ case TextArgument: return BaseValue::KD_Text;
+ case DateTimeArgument: return BaseValue::KD_DateTime;
+ }
+}
+
+bool TextFormatterNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_TextFormatting;
+}
+
+TextFormatterNode::TextFormatterNode()
+ : WorkflowNode(KD_TextFormatting, false)
+{
+}
+
+int TextFormatterNode::GetElementCount() const
+{
+ return mElements.size();
+}
+
+const TextFormatterNode::Element& TextFormatterNode::GetElement(int idx) const
+{
+ return mElements[idx];
+}
+
+void TextFormatterNode::SetElement(int idx, std::string text)
+{
+ assert(idx >= 0 && idx < mElements.size());
+
+ std::visit(
+ Overloaded{
+ [&](const std::string& original) { mMinOutputChars -= original.size(); },
+ [&](const Argument& original) { PreRemoveElement(idx); },
+ },
+ mElements[idx]);
+
+ mMinOutputChars += text.size();
+ mElements[idx] = std::move(text);
+}
+
+void TextFormatterNode::SetElement(int idx, ArgumentType argument)
+{
+ assert(idx >= 0 && idx < mElements.size());
+
+ std::visit(
+ Overloaded{
+ [&](const std::string& original) {
+ mMinOutputChars -= original.size();
+
+ mElements[idx] = Argument{
+ .Type = argument,
+ .PinIdx = Impl::FindPinForElement(mElements, idx),
+ };
+ /* `original` is invalid from this point */
+ },
+ [&](const Argument& original) {
+ int pinIdx = original.PinIdx;
+
+ // Create pin
+ auto& pin = mInputs[pinIdx];
+ pin.MatchingType = ArgumentTypeToValueKind(argument);
+
+ // Create element
+ mElements[idx] = Argument{
+ .Type = argument,
+ .PinIdx = pinIdx,
+ };
+ /* `original` is invalid from this point */
+ },
+ },
+ mElements[idx]);
+}
+
+void TextFormatterNode::InsertElement(int idx, std::string text)
+{
+ assert(idx >= 0);
+ if (idx >= mElements.size()) AppendElement(std::move(text));
+
+ mMinOutputChars += text.size();
+ mElements.insert(mElements.begin() + idx, std::move(text));
+}
+
+void TextFormatterNode::InsertElement(int idx, ArgumentType argument)
+{
+ assert(idx >= 0);
+ if (idx >= mElements.size()) AppendElement(argument);
+
+ int pinIdx = Impl::FindPinForElement(mElements, idx);
+
+ // Create pin
+ auto& pin = InsertInputPin(pinIdx);
+ pin.MatchingType = ArgumentTypeToValueKind(argument);
+
+ // Create element
+ mElements.insert(
+ mElements.begin() + idx,
+ Argument{
+ .Type = argument,
+ .PinIdx = pinIdx,
+ });
+}
+
+void TextFormatterNode::AppendElement(std::string text)
+{
+ mMinOutputChars += text.size();
+ mElements.push_back(std::move(text));
+}
+
+void TextFormatterNode::AppendElement(ArgumentType argument)
+{
+ int pinIdx = mInputs.size();
+ // Create pin
+ mInputs.push_back(InputPin{});
+ mInputs.back().MatchingType = ArgumentTypeToValueKind(argument);
+ // Creat eelement
+ mElements.push_back(Argument{
+ .Type = argument,
+ .PinIdx = pinIdx,
+ });
+}
+
+void TextFormatterNode::RemoveElement(int idx)
+{
+ assert(idx >= 0 && idx < mElements.size());
+
+ PreRemoveElement(idx);
+ if (auto arg = std::get_if<Argument>(&mElements[idx])) {
+ RemoveInputPin(arg->PinIdx);
+ }
+ mElements.erase(mElements.begin() + idx);
+}
+
+void TextFormatterNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+ std::string result;
+ result.reserve((size_t)(mMinOutputChars * 1.5f));
+
+ auto HandleText = [&](const std::string& str) {
+ result += str;
+ };
+ auto HandleArgument = [&](const Argument& arg) {
+ switch (arg.Type) {
+ case NumericArgument: {
+ if (auto val = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) {
+ result += val->GetString();
+ } else {
+ // TODO localize
+ ctx.ReportError("Non-numeric value connected to a numeric text format parameter.", *this);
+ }
+ } break;
+ case TextArgument: {
+ if (auto val = dyn_cast<TextValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) {
+ result += val->GetValue();
+ } else {
+ // TODO localize
+ ctx.ReportError("Non-text value connected to a textual text format parameter.", *this);
+ }
+ } break;
+ case DateTimeArgument: {
+ if (auto val = dyn_cast<DateTimeValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) {
+ result += val->GetString();
+ } else {
+ // TODO localize
+ ctx.ReportError("Non-date/time value connected to a date/time text format parameter.", *this);
+ }
+ } break;
+ }
+ };
+
+ for (auto& elm : mElements) {
+ std::visit(Overloaded{ HandleText, HandleArgument }, elm);
+ }
+}
+
+void TextFormatterNode::PreRemoveElement(int idx)
+{
+ auto& elm = mElements[idx];
+ if (auto arg = std::get_if<Argument>(&elm)) {
+ RemoveInputPin(arg->PinIdx);
+ Impl::ForArguments(
+ mElements.begin() + idx + 1,
+ mElements.end(),
+ [&](Argument& arg) {
+ arg.PinIdx--;
+ });
+ }
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp
new file mode 100644
index 0000000..4689931
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <cstddef>
+#include <memory>
+#include <variant>
+#include <vector>
+
+class TextFormatterNode : public WorkflowNode
+{
+public:
+ enum ArgumentType
+ {
+ NumericArgument,
+ TextArgument,
+ DateTimeArgument,
+ };
+
+private:
+ class Impl;
+
+ struct Argument
+ {
+ ArgumentType Type;
+ int PinIdx;
+ };
+ using Element = std::variant<std::string, Argument>;
+
+ std::vector<Element> mElements;
+ int mMinOutputChars;
+
+public:
+ static BaseValue::Kind ArgumentTypeToValueKind(ArgumentType arg);
+ static bool IsInstance(const WorkflowNode* node);
+ TextFormatterNode();
+
+ int GetElementCount() const;
+ const Element& GetElement(int idx) const;
+
+ void SetElement(int idx, std::string text);
+ void SetElement(int idx, ArgumentType argument);
+ void InsertElement(int idx, std::string text);
+ void InsertElement(int idx, ArgumentType argument);
+ void AppendElement(std::string text);
+ void AppendElement(ArgumentType argument);
+ void RemoveElement(int idx);
+
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+
+private:
+ void PreRemoveElement(int idx);
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp
new file mode 100644
index 0000000..93d458c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp
@@ -0,0 +1,32 @@
+#include "UserInputNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+
+bool FormInputNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_FormInput;
+}
+
+FormInputNode::FormInputNode()
+ : WorkflowNode(KD_FormInput, false)
+{
+}
+
+void FormInputNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
+
+bool DatabaseRowsInputNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_DatabaseRowsInput;
+}
+
+DatabaseRowsInputNode::DatabaseRowsInputNode()
+ : WorkflowNode(KD_DatabaseRowsInput, false)
+{
+}
+
+void DatabaseRowsInputNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp
new file mode 100644
index 0000000..f0b923c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+class FormInputNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ FormInputNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
+
+class DatabaseRowsInputNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ DatabaseRowsInputNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp
new file mode 100644
index 0000000..4153825
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+// DocumentNodes.hpp
+class DocumentTemplateExpansionNode;
+
+// InputNodes.hpp
+class FormInputNode;
+class DatabaseRowsInputNode;
+
+// NumericNodes.hpp
+class NumericOperationNode;
+class NumericExpressionNode;
+
+// TextNodes.hpp
+class TextFormatterNode;
diff --git a/app/source/Cplt/Model/Workflow/Value.hpp b/app/source/Cplt/Model/Workflow/Value.hpp
new file mode 100644
index 0000000..70fcb57
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Value.hpp
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <Cplt/Utils/Color.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <vector>
+
+class BaseValue
+{
+public:
+ enum Kind
+ {
+ KD_Numeric,
+ KD_Text,
+ KD_DateTime,
+ KD_DatabaseRowId,
+ KD_List,
+ KD_Dictionary,
+
+ KD_BaseObject,
+ KD_SaleDatabaseRow,
+ KD_PurchaseDatabaseRow,
+ KD_BaseObjectLast = KD_PurchaseDatabaseRow,
+
+ /// An unspecified type, otherwise known as "any" in some contexts.
+ InvalidKind,
+ KindCount = InvalidKind,
+ };
+
+ struct KindInfo
+ {
+ ImGui::IconType PinIcon;
+ RgbaColor PinColor;
+ };
+
+private:
+ Kind mKind;
+
+public:
+ static const KindInfo& QueryInfo(Kind kind);
+ static const char* Format(Kind kind);
+ static std::unique_ptr<BaseValue> CreateByKind(Kind kind);
+
+ static bool IsInstance(const BaseValue* value);
+
+ BaseValue(Kind kind);
+ virtual ~BaseValue() = default;
+
+ BaseValue(const BaseValue&) = delete;
+ BaseValue& operator=(const BaseValue&) = delete;
+ BaseValue(BaseValue&&) = default;
+ BaseValue& operator=(BaseValue&&) = default;
+
+ Kind GetKind() const;
+
+ // TODO get constant editor
+
+ /// The functions \c ReadFrom, \c WriteTo will only be valid to call if this function returns true.
+ virtual bool SupportsConstant() const;
+ virtual void ReadFrom(std::istream& stream);
+ virtual void WriteTo(std::ostream& stream);
+};
+
+class BaseObjectDescription
+{
+public:
+ struct Property
+ {
+ std::string Name;
+ BaseValue::Kind Kind;
+ bool Mutatable = true;
+ };
+
+public:
+ std::vector<Property> Properties;
+};
+
+class BaseObjectValue : public BaseValue
+{
+public:
+ /// \param kind A value kind enum, within the range of KD_BaseObject and KD_BaseObjectLast (both inclusive).
+ static const BaseObjectDescription& QueryObjectInfo(Kind kind);
+
+ static bool IsInstance(const BaseValue* value);
+ BaseObjectValue(Kind kind);
+
+ const BaseObjectDescription& GetObjectDescription() const;
+
+ virtual const BaseValue* GetProperty(int idx) const = 0;
+ virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value) = 0;
+};
diff --git a/app/source/Cplt/Model/Workflow/ValueInternals.hpp b/app/source/Cplt/Model/Workflow/ValueInternals.hpp
new file mode 100644
index 0000000..45842db
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/ValueInternals.hpp
@@ -0,0 +1,21 @@
+// This file contains utility classes and macros for implementing values
+// As consumers, you should not include this header as it contains unnecessary symbols and can pollute your files
+// for this reason, classes here aren't forward-declared in fwd.hpp either.
+
+#pragma once
+
+#include <Cplt/Utils/RTTI.hpp>
+
+#include <utility>
+
+#define CHECK_VALUE_TYPE(Type, value) \
+ if (!is_a<Type>(value)) { \
+ return false; \
+ }
+
+#define CHECK_VALUE_TYPE_AND_MOVE(Type, dest, value) \
+ if (auto ptr = dyn_cast<Type>(value)) { \
+ dest = std::move(*ptr); \
+ } else { \
+ return false; \
+ }
diff --git a/app/source/Cplt/Model/Workflow/Value_Main.cpp b/app/source/Cplt/Model/Workflow/Value_Main.cpp
new file mode 100644
index 0000000..ca972c4
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Value_Main.cpp
@@ -0,0 +1,35 @@
+#include "Value.hpp"
+
+BaseValue::BaseValue(Kind kind)
+ : mKind{ kind }
+{
+}
+
+BaseValue::Kind BaseValue::GetKind() const
+{
+ return mKind;
+}
+
+bool BaseValue::SupportsConstant() const
+{
+ return false;
+}
+
+void BaseValue::ReadFrom(std::istream& stream)
+{
+}
+
+void BaseValue::WriteTo(std::ostream& stream)
+{
+}
+
+BaseObjectValue::BaseObjectValue(Kind kind)
+ : BaseValue(kind)
+{
+ assert(kind >= KD_BaseObject && kind <= KD_BaseObjectLast);
+}
+
+const BaseObjectDescription& BaseObjectValue::GetObjectDescription() const
+{
+ return QueryObjectInfo(this->GetKind());
+}
diff --git a/app/source/Cplt/Model/Workflow/Value_RTTI.cpp b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp
new file mode 100644
index 0000000..a2a6960
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp
@@ -0,0 +1,174 @@
+#include "Value.hpp"
+
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/Model/Workflow/Values/Database.hpp>
+#include <Cplt/Model/Workflow/Values/Dictionary.hpp>
+#include <Cplt/Model/Workflow/Values/List.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+
+constexpr BaseValue::KindInfo kEmptyInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(),
+};
+
+constexpr BaseValue::KindInfo kNumericInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(147, 226, 74),
+};
+
+constexpr BaseValue::KindInfo kTextInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(124, 21, 153),
+};
+
+constexpr BaseValue::KindInfo kDateTimeInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(147, 226, 74),
+};
+
+constexpr BaseValue::KindInfo kDatabaseRowIdInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(216, 42, 221),
+};
+
+constexpr BaseValue::KindInfo kListInfo{
+ .PinIcon = ImGui::IconType::Diamond,
+ .PinColor = RgbaColor(58, 154, 214),
+};
+
+constexpr BaseValue::KindInfo kDictionaryInfo{
+ .PinIcon = ImGui::IconType::Diamond,
+ .PinColor = RgbaColor(240, 240, 34),
+};
+
+constexpr BaseValue::KindInfo kDatabaseRowInfo{
+ .PinIcon = ImGui::IconType::Square,
+ .PinColor = RgbaColor(15, 124, 196),
+};
+
+constexpr BaseValue::KindInfo kObjectInfo{
+ .PinIcon = ImGui::IconType::Square,
+ .PinColor = RgbaColor(161, 161, 161),
+};
+
+const BaseValue::KindInfo& BaseValue::QueryInfo(BaseValue::Kind kind)
+{
+ switch (kind) {
+ case KD_Numeric: return kNumericInfo;
+ case KD_Text: return kTextInfo;
+ case KD_DateTime: return kDateTimeInfo;
+ case KD_DatabaseRowId: return kDatabaseRowIdInfo;
+ case KD_List: return kListInfo;
+ case KD_Dictionary: return kDictionaryInfo;
+
+ case KD_BaseObject: return kObjectInfo;
+ case KD_SaleDatabaseRow:
+ case KD_PurchaseDatabaseRow:
+ return kDatabaseRowInfo;
+
+ case InvalidKind: break;
+ }
+ return kEmptyInfo;
+}
+
+const char* BaseValue::Format(Kind kind)
+{
+ switch (kind) {
+ case KD_Numeric: return I18N_TEXT("Numeric", L10N_VALUE_NUMERIC);
+ case KD_Text: return I18N_TEXT("Text", L10N_VALUE_TEXT);
+ case KD_DateTime: return I18N_TEXT("Date/time", L10N_VALUE_DATE_TIME);
+ case KD_DatabaseRowId: return I18N_TEXT("Row id", L10N_VALUE_ROW_ID);
+ case KD_List: return I18N_TEXT("List", L10N_VALUE_LIST);
+ case KD_Dictionary: return I18N_TEXT("Dictionary", L10N_VALUE_DICT);
+
+ case KD_BaseObject: return I18N_TEXT("Object", L10N_VALUE_OBJECT);
+ case KD_SaleDatabaseRow: return I18N_TEXT("Sale record", L10N_VALUE_SALE_RECORD);
+ case KD_PurchaseDatabaseRow: return I18N_TEXT("Purchase record", L10N_VALUE_PURCHASE_RECORD);
+
+ case InvalidKind: break;
+ }
+ return "";
+}
+
+std::unique_ptr<BaseValue> BaseValue::CreateByKind(BaseValue::Kind kind)
+{
+ switch (kind) {
+ case KD_Numeric: return std::make_unique<NumericValue>();
+ case KD_Text: return std::make_unique<TextValue>();
+ case KD_DateTime: return std::make_unique<DateTimeValue>();
+ case KD_DatabaseRowId: return std::make_unique<DatabaseRowIdValue>();
+ case KD_List: return std::make_unique<ListValue>();
+ case KD_Dictionary: return std::make_unique<DictionaryValue>();
+
+ case KD_BaseObject: return nullptr;
+ case KD_SaleDatabaseRow: return std::make_unique<SaleDatabaseRowValue>();
+ case KD_PurchaseDatabaseRow: return std::make_unique<PurchaseDatabaseRowValue>();
+
+ case InvalidKind: break;
+ }
+ return nullptr;
+}
+
+bool BaseValue::IsInstance(const BaseValue* value)
+{
+ return true;
+}
+
+const BaseObjectDescription kEmptyObjectInfo{
+ .Properties = {},
+};
+
+const BaseObjectDescription kSaleDbRowObject{
+ .Properties = {
+ {
+ .Name = I18N_TEXT("Customer", L10N_VALUE_PROPERTY_CUSTOMER),
+ .Kind = BaseValue::KD_Text,
+ .Mutatable = false,
+ },
+ {
+ .Name = I18N_TEXT("Deadline", L10N_VALUE_PROPERTY_DEADLINE),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ {
+ .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ },
+};
+
+const BaseObjectDescription kPurchaseDbRowObject{
+ .Properties = {
+ {
+ .Name = I18N_TEXT("Factory", L10N_VALUE_PROPERTY_FACTORY),
+ .Kind = BaseValue::KD_Text,
+ .Mutatable = false,
+ },
+ {
+ .Name = I18N_TEXT("Order time", L10N_VALUE_PROPERTY_ORDER_TIME),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ {
+ .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ },
+};
+
+const BaseObjectDescription& BaseObjectValue::QueryObjectInfo(Kind kind)
+{
+ switch (kind) {
+ case KD_BaseObject: return kEmptyObjectInfo;
+ case KD_SaleDatabaseRow: return kSaleDbRowObject;
+ case KD_PurchaseDatabaseRow: return kPurchaseDbRowObject;
+
+ default: break;
+ }
+ return kEmptyObjectInfo;
+}
+
+bool BaseObjectValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() >= KD_BaseObject &&
+ value->GetKind() <= KD_BaseObjectLast;
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.cpp b/app/source/Cplt/Model/Workflow/Values/Basic.cpp
new file mode 100644
index 0000000..198387c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Basic.cpp
@@ -0,0 +1,111 @@
+#include "Basic.hpp"
+
+#include <charconv>
+#include <cmath>
+#include <limits>
+
+bool NumericValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_Numeric;
+}
+
+NumericValue::NumericValue()
+ : BaseValue(BaseValue::KD_Numeric)
+{
+}
+
+template <class T, int kMaxSize>
+static std::string NumberToString(T value)
+{
+ char buf[kMaxSize];
+ auto res = std::to_chars(buf, buf + kMaxSize, value);
+ if (res.ec == std::errc()) {
+ return std::string(buf, res.ptr);
+ } else {
+ return "<err>";
+ }
+}
+
+std::string NumericValue::GetTruncatedString() const
+{
+ constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10;
+ return ::NumberToString<int64_t, kMaxSize>((int64_t)mValue);
+}
+
+std::string NumericValue::GetRoundedString() const
+{
+ constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10;
+ return ::NumberToString<int64_t, kMaxSize>((int64_t)std::round(mValue));
+}
+
+std::string NumericValue::GetString() const
+{
+ constexpr auto kMaxSize = std::numeric_limits<double>::max_digits10;
+ return ::NumberToString<double, kMaxSize>(mValue);
+}
+
+int64_t NumericValue::GetInt() const
+{
+ return static_cast<int64_t>(mValue);
+}
+
+double NumericValue::GetValue() const
+{
+ return mValue;
+}
+
+void NumericValue::SetValue(double value)
+{
+ mValue = value;
+}
+
+bool TextValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_Text;
+}
+
+TextValue::TextValue()
+ : BaseValue(BaseValue::KD_Text)
+{
+}
+
+const std::string& TextValue::GetValue() const
+{
+ return mValue;
+}
+
+void TextValue::SetValue(const std::string& value)
+{
+ mValue = value;
+}
+
+bool DateTimeValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_DateTime;
+}
+
+DateTimeValue::DateTimeValue()
+ : BaseValue(BaseValue::KD_DateTime)
+{
+}
+
+std::string DateTimeValue::GetString() const
+{
+ namespace chrono = std::chrono;
+ auto t = chrono::system_clock::to_time_t(mValue);
+
+ char data[32];
+ std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
+
+ return std::string(data);
+}
+
+const std::chrono::time_point<std::chrono::system_clock>& DateTimeValue::GetValue() const
+{
+ return mValue;
+}
+
+void DateTimeValue::SetValue(const std::chrono::time_point<std::chrono::system_clock>& value)
+{
+ mValue = value;
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.hpp b/app/source/Cplt/Model/Workflow/Values/Basic.hpp
new file mode 100644
index 0000000..820fb13
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Basic.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+
+#include <chrono>
+#include <cstdint>
+#include <string>
+
+class NumericValue : public BaseValue
+{
+private:
+ double mValue;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ NumericValue();
+
+ NumericValue(const NumericValue&) = delete;
+ NumericValue& operator=(const NumericValue&) = delete;
+ NumericValue(NumericValue&&) = default;
+ NumericValue& operator=(NumericValue&&) = default;
+
+ std::string GetTruncatedString() const;
+ std::string GetRoundedString() const;
+ std::string GetString() const;
+
+ int64_t GetInt() const;
+ double GetValue() const;
+ void SetValue(double value);
+};
+
+class TextValue : public BaseValue
+{
+private:
+ std::string mValue;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ TextValue();
+
+ TextValue(const TextValue&) = delete;
+ TextValue& operator=(const TextValue&) = delete;
+ TextValue(TextValue&&) = default;
+ TextValue& operator=(TextValue&&) = default;
+
+ const std::string& GetValue() const;
+ void SetValue(const std::string& value);
+};
+
+class DateTimeValue : public BaseValue
+{
+private:
+ std::chrono::time_point<std::chrono::system_clock> mValue;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ DateTimeValue();
+
+ DateTimeValue(const DateTimeValue&) = delete;
+ DateTimeValue& operator=(const DateTimeValue&) = delete;
+ DateTimeValue(DateTimeValue&&) = default;
+ DateTimeValue& operator=(DateTimeValue&&) = default;
+
+ std::string GetString() const;
+ const std::chrono::time_point<std::chrono::system_clock>& GetValue() const;
+ void SetValue(const std::chrono::time_point<std::chrono::system_clock>& value);
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/Database.cpp b/app/source/Cplt/Model/Workflow/Values/Database.cpp
new file mode 100644
index 0000000..25b77e9
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Database.cpp
@@ -0,0 +1,88 @@
+#include "Database.hpp"
+
+#include <Cplt/Model/Database.hpp>
+#include <Cplt/Model/Workflow/ValueInternals.hpp>
+
+#include <limits>
+
+TableKind DatabaseRowIdValue::GetTable() const
+{
+ return mTable;
+}
+
+int64_t DatabaseRowIdValue::GetRowId() const
+{
+ return mRowId;
+}
+
+bool DatabaseRowIdValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_DatabaseRowId;
+}
+
+DatabaseRowIdValue::DatabaseRowIdValue()
+ : BaseValue(KD_DatabaseRowId)
+ , mTable{ TableKind::Sales }
+ , mRowId{ std::numeric_limits<int64_t>::max() }
+{
+}
+
+bool SaleDatabaseRowValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_SaleDatabaseRow;
+}
+
+SaleDatabaseRowValue::SaleDatabaseRowValue()
+ : BaseObjectValue(KD_SaleDatabaseRow)
+{
+}
+
+const BaseValue* SaleDatabaseRowValue::GetProperty(int idx) const
+{
+ switch (idx) {
+ case 0: return &mCustomerName;
+ case 1: return &mDeadline;
+ case 2: return &mDeliveryTime;
+ default: return nullptr;
+ }
+}
+
+bool SaleDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value)
+{
+ switch (idx) {
+ case 0: return false;
+ case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeadline, value.get()); break;
+ case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break;
+ }
+ return true;
+}
+
+bool PurchaseDatabaseRowValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_PurchaseDatabaseRow;
+}
+
+PurchaseDatabaseRowValue::PurchaseDatabaseRowValue()
+ : BaseObjectValue(KD_PurchaseDatabaseRow)
+{
+}
+
+const BaseValue* PurchaseDatabaseRowValue::GetProperty(int idx) const
+{
+ switch (idx) {
+ case 0: return &mFactoryName;
+ case 1: return &mOrderTime;
+ case 2: return &mDeliveryTime;
+ default: return nullptr;
+ }
+}
+
+bool PurchaseDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value)
+{
+ switch (idx) {
+ case 0: return false;
+ case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mOrderTime, value.get()); break;
+ case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break;
+ }
+ return true;
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Database.hpp b/app/source/Cplt/Model/Workflow/Values/Database.hpp
new file mode 100644
index 0000000..f1c1571
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Database.hpp
@@ -0,0 +1,51 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/fwd.hpp>
+
+class DatabaseRowIdValue : public BaseValue
+{
+private:
+ TableKind mTable;
+ int64_t mRowId;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ DatabaseRowIdValue();
+
+ TableKind GetTable() const;
+ int64_t GetRowId() const;
+};
+
+class SaleDatabaseRowValue : public BaseObjectValue
+{
+private:
+ int mCustomerId;
+ TextValue mCustomerName;
+ DateTimeValue mDeadline;
+ DateTimeValue mDeliveryTime;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ SaleDatabaseRowValue();
+
+ virtual const BaseValue* GetProperty(int idx) const;
+ virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value);
+};
+
+class PurchaseDatabaseRowValue : public BaseObjectValue
+{
+private:
+ int mFactoryId;
+ TextValue mFactoryName;
+ DateTimeValue mOrderTime;
+ DateTimeValue mDeliveryTime;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ PurchaseDatabaseRowValue();
+
+ virtual const BaseValue* GetProperty(int idx) const;
+ virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value);
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp
new file mode 100644
index 0000000..97bf509
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp
@@ -0,0 +1,49 @@
+#include "Dictionary.hpp"
+
+#include <Cplt/Utils/Macros.hpp>
+
+bool DictionaryValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_Dictionary;
+}
+
+DictionaryValue::DictionaryValue()
+ : BaseValue(KD_Dictionary)
+{
+}
+
+int DictionaryValue::GetCount() const
+{
+ return mElements.size();
+}
+
+BaseValue* DictionaryValue::Find(std::string_view key)
+{
+ auto iter = mElements.find(key);
+ if (iter != mElements.end()) {
+ return iter.value().get();
+ } else {
+ return nullptr;
+ }
+}
+
+BaseValue* DictionaryValue::Insert(std::string_view key, std::unique_ptr<BaseValue>& value)
+{
+ auto [iter, success] = mElements.insert(key, std::move(value));
+ if (success) {
+ return iter.value().get();
+ } else {
+ return nullptr;
+ }
+}
+
+BaseValue& DictionaryValue::InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value)
+{
+ auto [iter, DISCARD] = mElements.emplace(key, std::move(value));
+ return *iter.value();
+}
+
+void DictionaryValue::Remove(std::string_view key)
+{
+ mElements.erase(mElements.find(key));
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp
new file mode 100644
index 0000000..6eff308
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+
+#include <tsl/array_map.h>
+#include <memory>
+#include <string>
+#include <string_view>
+
+class DictionaryValue : public BaseValue
+{
+private:
+ tsl::array_map<char, std::unique_ptr<BaseValue>> mElements;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ DictionaryValue();
+
+ int GetCount() const;
+ BaseValue* Find(std::string_view key);
+
+ BaseValue* Insert(std::string_view key, std::unique_ptr<BaseValue>& value);
+ BaseValue& InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value);
+ void Remove(std::string_view key);
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/List.cpp b/app/source/Cplt/Model/Workflow/Values/List.cpp
new file mode 100644
index 0000000..9fd6bfd
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/List.cpp
@@ -0,0 +1,100 @@
+#include "List.hpp"
+
+#include <utility>
+
+BaseValue* ListValue::Iterator::operator*() const
+{
+ return mIter->get();
+}
+
+BaseValue* ListValue::Iterator::operator->() const
+{
+ return mIter->get();
+}
+
+ListValue::Iterator& ListValue::Iterator::operator++()
+{
+ ++mIter;
+ return *this;
+}
+
+ListValue::Iterator ListValue::Iterator::operator++(int) const
+{
+ return Iterator(mIter + 1);
+}
+
+ListValue::Iterator& ListValue::Iterator::operator--()
+{
+ --mIter;
+ return *this;
+}
+
+ListValue::Iterator ListValue::Iterator::operator--(int) const
+{
+ return Iterator(mIter - 1);
+}
+
+bool operator==(const ListValue::Iterator& a, const ListValue::Iterator& b)
+{
+ return a.mIter == b.mIter;
+}
+
+ListValue::Iterator::Iterator(decltype(mIter) iter)
+ : mIter{ iter }
+{
+}
+
+bool ListValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_List;
+}
+
+ListValue::ListValue()
+ : BaseValue(KD_List)
+{
+}
+
+int ListValue::GetCount() const
+{
+ return mElements.size();
+}
+
+BaseValue* ListValue::GetElement(int i) const
+{
+ return mElements[i].get();
+}
+
+void ListValue::Append(std::unique_ptr<BaseValue> element)
+{
+ mElements.push_back(std::move(element));
+}
+
+void ListValue::Insert(int i, std::unique_ptr<BaseValue> element)
+{
+ mElements.insert(mElements.begin() + i, std::move(element));
+}
+
+void ListValue::Insert(Iterator iter, std::unique_ptr<BaseValue> element)
+{
+ mElements.insert(iter.mIter, std::move(element));
+}
+
+void ListValue::Remove(int i)
+{
+ mElements.erase(mElements.begin() + i);
+}
+
+void ListValue::Remove(Iterator iter)
+{
+ mElements.erase(iter.mIter);
+}
+
+ListValue::Iterator ListValue::begin()
+{
+ return Iterator(mElements.begin());
+}
+
+ListValue::Iterator ListValue::end()
+{
+ return Iterator(mElements.end());
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/List.hpp b/app/source/Cplt/Model/Workflow/Values/List.hpp
new file mode 100644
index 0000000..cc8e061
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/List.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+
+#include <memory>
+#include <vector>
+
+class ListValue : public BaseValue
+{
+public:
+ class Iterator
+ {
+ private:
+ std::vector<std::unique_ptr<BaseValue>>::iterator mIter;
+
+ public:
+ BaseValue* operator*() const;
+ BaseValue* operator->() const;
+
+ Iterator& operator++();
+ Iterator operator++(int) const;
+ Iterator& operator--();
+ Iterator operator--(int) const;
+
+ friend bool operator==(const Iterator& a, const Iterator& b);
+
+ private:
+ friend class ListValue;
+ Iterator(decltype(mIter) iter);
+ };
+
+private:
+ std::vector<std::unique_ptr<BaseValue>> mElements;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ ListValue();
+
+ int GetCount() const;
+ BaseValue* GetElement(int i) const;
+
+ void Append(std::unique_ptr<BaseValue> element);
+ void Insert(int i, std::unique_ptr<BaseValue> element);
+ void Insert(Iterator iter, std::unique_ptr<BaseValue> element);
+ void Remove(int i);
+ void Remove(Iterator iter);
+
+ Iterator begin();
+ Iterator end();
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/fwd.hpp b/app/source/Cplt/Model/Workflow/Values/fwd.hpp
new file mode 100644
index 0000000..51a04e9
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/fwd.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+// Basic.hpp
+class NumericValue;
+class TextValue;
+class DateTimeValue;
+
+// Database.hpp
+class DatabaseRowIdValue;
+class SaleDatabaseRowValue;
+class PurchaseDatabaseRowValue;
+
+// Dictionary.hpp
+class DictionaryValue;
+
+// List.hpp
+class ListValue;
diff --git a/app/source/Cplt/Model/Workflow/Workflow.hpp b/app/source/Cplt/Model/Workflow/Workflow.hpp
new file mode 100644
index 0000000..e075e3c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow.hpp
@@ -0,0 +1,316 @@
+#pragma once
+
+#include <Cplt/Model/Assets.hpp>
+#include <Cplt/Model/Workflow/Value.hpp>
+#include <Cplt/Utils/Vector.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <imgui_node_editor.h>
+#include <cstddef>
+#include <cstdint>
+#include <filesystem>
+#include <functional>
+#include <iosfwd>
+#include <limits>
+#include <memory>
+#include <span>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace ImNodes = ax::NodeEditor;
+
+class WorkflowConnection
+{
+public:
+ static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max();
+
+ uint32_t Id;
+ uint32_t SourceNode;
+ uint32_t SourcePin;
+ uint32_t DestinationNode;
+ uint32_t DestinationPin;
+
+public:
+ WorkflowConnection();
+
+ bool IsValid() const;
+
+ /// Used for `LinkId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading).
+ ImNodes::LinkId GetLinkId() const;
+
+ void DrawDebugInfo() const;
+ void ReadFrom(std::istream& stream);
+ void WriteTo(std::ostream& stream) const;
+};
+
+class WorkflowNode
+{
+public:
+ static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max();
+ static constexpr auto kInvalidPinId = std::numeric_limits<uint32_t>::max();
+
+ enum Type
+ {
+ InputType,
+ TransformType,
+ OutputType,
+ };
+
+ enum Kind
+ {
+ KD_NumericAddition,
+ KD_NumericSubtraction,
+ KD_NumericMultiplication,
+ KD_NumericDivision,
+ KD_NumericExpression,
+ KD_TextFormatting,
+ KD_DocumentTemplateExpansion,
+ KD_FormInput,
+ KD_DatabaseRowsInput,
+
+ InvalidKind,
+ KindCount = InvalidKind,
+ };
+
+ enum Category
+ {
+ CG_Numeric,
+ CG_Text,
+ CG_Document,
+ CG_UserInput,
+ CG_SystemInput,
+ CG_Output,
+
+ InvalidCategory,
+ CategoryCount = InvalidCategory,
+ };
+
+ struct InputPin
+ {
+ uint32_t Connection = WorkflowConnection::kInvalidId;
+ BaseValue::Kind MatchingType = BaseValue::InvalidKind;
+ bool ConnectionToConst = false;
+
+ /// A constant connection connects from a user-specified constant value, feeding to a valid \c DestinationNode and \c DestinationPin (i.e. input pins).
+ bool IsConstantConnection() const;
+ bool IsConnected() const;
+ BaseValue::Kind GetMatchingType() const;
+ };
+
+ struct OutputPin
+ {
+ uint32_t Connection = WorkflowConnection::kInvalidId;
+ BaseValue::Kind MatchingType = BaseValue::InvalidKind;
+
+ bool IsConnected() const;
+ BaseValue::Kind GetMatchingType() const;
+ };
+
+protected:
+ friend class Workflow;
+ friend class WorkflowEvaluationContext;
+
+ Workflow* mWorkflow;
+ std::vector<InputPin> mInputs;
+ std::vector<OutputPin> mOutputs;
+ Vec2i mPosition;
+ uint32_t mId;
+ Kind mKind;
+ int mDepth;
+ bool mLocked;
+
+public:
+ static const char* FormatKind(Kind kind);
+ static const char* FormatCategory(Category category);
+ static const char* FormatType(Type type);
+ static Category QueryCategory(Kind kind);
+ static std::span<const Kind> QueryCategoryMembers(Category category);
+ static std::unique_ptr<WorkflowNode> CreateByKind(Kind kind);
+
+ static bool IsInstance(const WorkflowNode* node);
+
+ WorkflowNode(Kind kind, bool locked);
+ virtual ~WorkflowNode() = default;
+
+ WorkflowNode(const WorkflowNode&) = delete;
+ WorkflowNode& operator=(const WorkflowNode&) = delete;
+ WorkflowNode(WorkflowNode&&) = default;
+ WorkflowNode& operator=(WorkflowNode&&) = default;
+
+ void SetPosition(const Vec2i& position);
+ Vec2i GetPosition() const;
+
+ uint32_t GetId() const;
+ /// Used for `NodeId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading).
+ ImNodes::NodeId GetNodeId() const;
+ Kind GetKind() const;
+ int GetDepth() const;
+ bool IsLocked() const;
+
+ Type GetType() const;
+ bool IsInputNode() const;
+ bool IsOutputNode() const;
+
+ void ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId);
+ void DisconnectInput(uint32_t pinId);
+
+ void DrawInputPinDebugInfo(uint32_t pinId) const;
+ const InputPin& GetInputPin(uint32_t pinId) const;
+ ImNodes::PinId GetInputPinUniqueId(uint32_t pinId) const;
+
+ void ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId);
+ void DisconnectOutput(uint32_t pinId);
+
+ void DrawOutputPinDebugInfo(uint32_t pinId) const;
+ const OutputPin& GetOutputPin(uint32_t pinId) const;
+ ImNodes::PinId GetOutputPinUniqueId(uint32_t pinId) const;
+
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) = 0;
+
+ void Draw();
+ virtual void DrawExtra() {}
+
+ void DrawDebugInfo() const;
+ virtual void DrawExtraDebugInfo() const {}
+
+ virtual void ReadFrom(std::istream& istream);
+ virtual void WriteTo(std::ostream& ostream);
+
+protected:
+ InputPin& InsertInputPin(int atIdx);
+ void RemoveInputPin(int pin);
+ void SwapInputPin(int a, int b);
+ OutputPin& InsertOutputPin(int atIdx);
+ void RemoveOutputPin(int pin);
+ void SwapOutputPin(int a, int b);
+
+ /* For \c Workflow to invoke, override by implementations */
+
+ void OnAttach(Workflow& workflow, uint32_t newId);
+ void OnDetach();
+};
+
+class Workflow : public Asset
+{
+ friend class WorkflowNode;
+ friend class WorkflowEvaluationContext;
+ class Private;
+
+public:
+ using CategoryType = WorkflowAssetList;
+ static constinit const WorkflowAssetList Category;
+
+private:
+ std::vector<WorkflowConnection> mConnections;
+ std::vector<std::unique_ptr<WorkflowNode>> mNodes;
+ std::vector<std::unique_ptr<BaseValue>> mConstants;
+ std::vector<std::vector<uint32_t>> mDepthGroups;
+ int mConnectionCount;
+ int mNodeCount;
+ int mConstantCount;
+ bool mDepthsDirty = true;
+
+public:
+ /* Graph access */
+
+ const std::vector<WorkflowConnection>& GetConnections() const;
+ std::vector<WorkflowConnection>& GetConnections();
+ const std::vector<std::unique_ptr<WorkflowNode>>& GetNodes() const;
+ std::vector<std::unique_ptr<WorkflowNode>>& GetNodes();
+ const std::vector<std::unique_ptr<BaseValue>>& GetConstants() const;
+ std::vector<std::unique_ptr<BaseValue>>& GetConstants();
+
+ WorkflowConnection* GetConnectionById(uint32_t id);
+ WorkflowConnection* GetConnectionByLinkId(ImNodes::LinkId linkId);
+ WorkflowNode* GetNodeById(uint32_t id);
+ WorkflowNode* GetNodeByNodeId(ImNodes::NodeId nodeId);
+ BaseValue* GetConstantById(uint32_t id);
+
+ struct GlobalPinId
+ {
+ WorkflowNode* Node;
+ uint32_t PinId;
+ /// true => input pin
+ /// false => output pin
+ bool IsOutput;
+ };
+
+ /// `pinId` should be the `UniqueId` of a pin from a node that's within this workflow.
+ GlobalPinId DisassembleGlobalPinId(ImNodes::PinId id);
+ ImNodes::PinId FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const;
+
+ const std::vector<std::vector<uint32_t>>& GetDepthGroups() const;
+ bool DoesDepthNeedsUpdate() const;
+
+ /* Graph mutation */
+
+ void AddNode(std::unique_ptr<WorkflowNode> step);
+ void RemoveNode(uint32_t id);
+
+ void RemoveConnection(uint32_t id);
+
+ bool Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin);
+ bool DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin);
+ bool DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin);
+
+ /* Graph rebuild */
+
+ enum GraphUpdateResult
+ {
+ /// Successfully rebuilt graph dependent data.
+ /// Details: nothing is written.
+ GUR_Success,
+ /// Nothing has changed since last time UpdateGraph() was called.
+ /// Details: nothing is written.
+ GUR_NoWorkToDo,
+ /// Details: list of nodes is written.
+ GUR_UnsatisfiedDependencies,
+ /// Details: list of nodes is written.
+ GUR_UnreachableNodes,
+ };
+
+ using GraphUpdateDetails = std::variant<
+ // Case: nothing
+ std::monostate,
+ // Case: list of nodes (ids)
+ std::vector<uint32_t>>;
+
+ GraphUpdateResult UpdateGraph(GraphUpdateDetails* details = nullptr);
+
+ /* Serialization */
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+
+private:
+ std::pair<WorkflowConnection&, uint32_t> AllocWorkflowConnection();
+ std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> AllocWorkflowStep();
+};
+
+class WorkflowAssetList final : public AssetListTyped<Workflow>
+{
+private:
+ // AC = Asset Creator
+ std::string mACNewName;
+ NameSelectionError mACNewNameError = NameSelectionError::Empty;
+
+public:
+ // Inherit constructors
+ using AssetListTyped::AssetListTyped;
+
+protected:
+ void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override;
+
+ std::string RetrieveNameFromFile(const std::filesystem::path& file) const override;
+ uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override;
+ std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override;
+
+ bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override;
+ Workflow* LoadInstance(const SavedAsset& assetInfo) const override;
+ Workflow* CreateInstance(const SavedAsset& assetInfo) const override;
+ bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override;
+
+ void DisplayAssetCreator(ListState& state) override;
+ void DisplayDetailsTable(ListState& state) const override;
+};
diff --git a/app/source/Cplt/Model/Workflow/Workflow_Main.cpp b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
new file mode 100644
index 0000000..0f35b32
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
@@ -0,0 +1,846 @@
+#include "Workflow.hpp"
+
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/IO/Archive.hpp>
+#include <Cplt/Utils/UUID.hpp>
+
+#include <imgui.h>
+#include <imgui_node_editor.h>
+#include <imgui_stdlib.h>
+#include <tsl/robin_set.h>
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <fstream>
+#include <iostream>
+#include <queue>
+#include <utility>
+
+using namespace std::literals::string_view_literals;
+namespace fs = std::filesystem;
+namespace ImNodes = ax::NodeEditor;
+
+WorkflowConnection::WorkflowConnection()
+ : Id{ 0 }
+ , SourceNode{ WorkflowNode::kInvalidId }
+ , SourcePin{ WorkflowNode::kInvalidPinId }
+ , DestinationNode{ WorkflowNode::kInvalidId }
+ , DestinationPin{ WorkflowNode::kInvalidPinId }
+{
+}
+
+bool WorkflowConnection::IsValid() const
+{
+ return Id != 0;
+}
+
+ImNodes::LinkId WorkflowConnection::GetLinkId() const
+{
+ // Our id is 0-based (represents an index directly)
+ // but imgui-node-editor uses the value 0 to represent a null id, so we need to offset by 1
+ return Id + 1;
+}
+
+void WorkflowConnection::DrawDebugInfo() const
+{
+ ImGui::Text("Source (node with output pin):");
+ ImGui::Text("{ Node = %u, Pin = %u }", SourceNode, SourcePin);
+ ImGui::Text("Destination (node with input pin):");
+ ImGui::Text("{ Node = %u, Pin = %u }", DestinationNode, DestinationPin);
+}
+
+void WorkflowConnection::ReadFrom(std::istream& stream)
+{
+ stream >> SourceNode >> SourcePin;
+ stream >> DestinationNode >> DestinationPin;
+}
+
+void WorkflowConnection::WriteTo(std::ostream& stream) const
+{
+ stream << SourceNode << SourcePin;
+ stream << DestinationNode << DestinationPin;
+}
+
+bool WorkflowNode::InputPin::IsConstantConnection() const
+{
+ return ConnectionToConst && IsConnected();
+}
+
+bool WorkflowNode::InputPin::IsConnected() const
+{
+ return Connection != WorkflowConnection::kInvalidId;
+}
+
+BaseValue::Kind WorkflowNode::InputPin::GetMatchingType() const
+{
+ return MatchingType;
+}
+
+bool WorkflowNode::OutputPin::IsConnected() const
+{
+ return Connection != WorkflowConnection::kInvalidId;
+}
+
+BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const
+{
+ return MatchingType;
+}
+
+WorkflowNode::WorkflowNode(Kind kind, bool locked)
+ : mKind{ kind }
+ , mDepth{ -1 }
+ , mLocked(locked)
+{
+}
+
+Vec2i WorkflowNode::GetPosition() const
+{
+ return mPosition;
+}
+
+void WorkflowNode::SetPosition(const Vec2i& position)
+{
+ mPosition = position;
+}
+
+uint32_t WorkflowNode::GetId() const
+{
+ return mId;
+}
+
+ImNodes::NodeId WorkflowNode::GetNodeId() const
+{
+ // See WorkflowConnection::GetLinkId for the rationale
+ return mId + 1;
+}
+
+WorkflowNode::Kind WorkflowNode::GetKind() const
+{
+ return mKind;
+}
+
+int WorkflowNode::GetDepth() const
+{
+ return mDepth;
+}
+
+bool WorkflowNode::IsLocked() const
+{
+ return mLocked;
+}
+
+WorkflowNode::Type WorkflowNode::GetType() const
+{
+ if (IsInputNode()) {
+ return InputType;
+ } else if (IsOutputNode()) {
+ return OutputType;
+ } else {
+ return TransformType;
+ }
+}
+
+bool WorkflowNode::IsInputNode() const
+{
+ return mInputs.size() == 0;
+}
+
+bool WorkflowNode::IsOutputNode() const
+{
+ return mOutputs.size() == 0;
+}
+
+void WorkflowNode::ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId)
+{
+ mWorkflow->Connect(*this, pinId, srcNode, srcPinId);
+}
+
+void WorkflowNode::DisconnectInput(uint32_t pinId)
+{
+ mWorkflow->DisconnectByDestination(*this, pinId);
+}
+
+void WorkflowNode::DrawInputPinDebugInfo(uint32_t pinId) const
+{
+ ImGui::Text("Node ID: %d", mId);
+ ImGui::Text("Pin ID: (input) %d", pinId);
+}
+
+const WorkflowNode::InputPin& WorkflowNode::GetInputPin(uint32_t pinId) const
+{
+ return mInputs[pinId];
+}
+
+ImNodes::PinId WorkflowNode::GetInputPinUniqueId(uint32_t pinId) const
+{
+ return mWorkflow->FabricateGlobalPinId(*this, pinId, false);
+}
+
+void WorkflowNode::ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId)
+{
+ mWorkflow->Connect(dstNode, dstPinId, *this, pinId);
+}
+
+void WorkflowNode::DisconnectOutput(uint32_t pinId)
+{
+ mWorkflow->DisconnectBySource(*this, pinId);
+}
+
+void WorkflowNode::DrawOutputPinDebugInfo(uint32_t pinId) const
+{
+ ImGui::Text("Node ID: %d", mId);
+ ImGui::Text("Pin ID: (output) %d", pinId);
+}
+
+const WorkflowNode::OutputPin& WorkflowNode::GetOutputPin(uint32_t pinId) const
+{
+ return mOutputs[pinId];
+}
+
+ImNodes::PinId WorkflowNode::GetOutputPinUniqueId(uint32_t pinId) const
+{
+ return mWorkflow->FabricateGlobalPinId(*this, pinId, true);
+}
+
+void WorkflowNode::Draw()
+{
+ for (uint32_t i = 0; i < mInputs.size(); ++i) {
+ auto& pin = mInputs[i];
+ auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType);
+ ImNodes::BeginPin(GetInputPinUniqueId(i), ImNodes::PinKind::Input);
+ // TODO
+ ImNodes::EndPin();
+ }
+ for (uint32_t i = 0; i < mOutputs.size(); ++i) {
+ auto& pin = mOutputs[i];
+ auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType);
+ ImNodes::BeginPin(GetOutputPinUniqueId(i), ImNodes::PinKind::Output);
+ // TODO
+ ImNodes::EndPin();
+ }
+}
+
+void WorkflowNode::DrawDebugInfo() const
+{
+ ImGui::Text("Node kind: %s", FormatKind(mKind));
+ ImGui::Text("Node type: %s", FormatType(GetType()));
+ ImGui::Text("Node ID: %u", mId);
+ ImGui::Text("Depth: %d", mDepth);
+ DrawExtraDebugInfo();
+}
+
+void WorkflowNode::ReadFrom(std::istream& stream)
+{
+ stream >> mId;
+ stream >> mPosition.x >> mPosition.y;
+}
+
+void WorkflowNode::WriteTo(std::ostream& stream)
+{
+ stream << mId;
+ stream << mPosition.x << mPosition.y;
+}
+
+WorkflowNode::InputPin& WorkflowNode::InsertInputPin(int atIdx)
+{
+ assert(atIdx >= 0 && atIdx < mInputs.size());
+
+ mInputs.push_back(InputPin{});
+ for (int i = (int)mInputs.size() - 1, end = atIdx + 1; i >= end; --i) {
+ SwapInputPin(i, i + 1);
+ }
+
+ return mInputs[atIdx];
+}
+
+void WorkflowNode::RemoveInputPin(int pin)
+{
+ DisconnectInput(pin);
+ for (int i = 0, end = (int)mInputs.size() - 1; i < end; ++i) {
+ SwapInputPin(i, i + 1);
+ }
+ mInputs.resize(mInputs.size() - 1);
+}
+
+void WorkflowNode::SwapInputPin(int a, int b)
+{
+ auto& pinA = mInputs[a];
+ auto& pinB = mInputs[b];
+
+ if (mWorkflow) {
+ if (pinA.IsConnected() && !pinA.IsConstantConnection()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinA.Connection);
+ conn.DestinationPin = b;
+ }
+ if (pinB.IsConnected() && !pinB.IsConstantConnection()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinB.Connection);
+ conn.DestinationPin = a;
+ }
+ }
+
+ std::swap(pinA, pinB);
+}
+
+WorkflowNode::OutputPin& WorkflowNode::InsertOutputPin(int atIdx)
+{
+ assert(atIdx >= 0 && atIdx < mOutputs.size());
+
+ mOutputs.push_back(OutputPin{});
+ for (int i = (int)mOutputs.size() - 1, end = atIdx + 1; i >= end; --i) {
+ SwapOutputPin(i, i + 1);
+ }
+
+ return mOutputs[atIdx];
+}
+
+void WorkflowNode::RemoveOutputPin(int pin)
+{
+ DisconnectOutput(pin);
+ for (int i = 0, end = (int)mOutputs.size() - 1; i < end; ++i) {
+ SwapInputPin(i, i + 1);
+ }
+ mOutputs.resize(mOutputs.size() - 1);
+}
+
+void WorkflowNode::SwapOutputPin(int a, int b)
+{
+ auto& pinA = mOutputs[a];
+ auto& pinB = mOutputs[b];
+
+ if (mWorkflow) {
+ if (pinA.IsConnected()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinA.Connection);
+ conn.SourcePin = b;
+ }
+ if (pinB.IsConnected()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinB.Connection);
+ conn.SourcePin = a;
+ }
+ }
+
+ std::swap(pinA, pinB);
+}
+
+void WorkflowNode::OnAttach(Workflow& workflow, uint32_t newId)
+{
+}
+
+void WorkflowNode::OnDetach()
+{
+}
+
+const std::vector<WorkflowConnection>& Workflow::GetConnections() const
+{
+ return mConnections;
+}
+
+std::vector<WorkflowConnection>& Workflow::GetConnections()
+{
+ return mConnections;
+}
+
+const std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() const
+{
+ return mNodes;
+}
+
+std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes()
+{
+ return mNodes;
+}
+
+const std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() const
+{
+ return mConstants;
+}
+
+std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants()
+{
+ return mConstants;
+}
+
+WorkflowConnection* Workflow::GetConnectionById(uint32_t id)
+{
+ return &mConnections[id];
+}
+
+WorkflowConnection* Workflow::GetConnectionByLinkId(ImNodes::LinkId id)
+{
+ return &mConnections[(uint32_t)(size_t)id - 1];
+}
+
+WorkflowNode* Workflow::GetNodeById(uint32_t id)
+{
+ return mNodes[id].get();
+}
+
+WorkflowNode* Workflow::GetNodeByNodeId(ImNodes::NodeId id)
+{
+ return mNodes[(uint32_t)(size_t)id - 1].get();
+}
+
+BaseValue* Workflow::GetConstantById(uint32_t id)
+{
+ return mConstants[id].get();
+}
+
+Workflow::GlobalPinId Workflow::DisassembleGlobalPinId(ImNodes::PinId pinId)
+{
+ // imgui-node-editor requires all pins to have a global, unique id
+ // but in our model the pin are typed (input vs output) and associated with a node: there is no built-in global id
+ // Therefore we encode one ourselves
+
+ // Global pin id format
+ // nnnnnnnn nnnnnnnn nnnnnnnn nnnnnnnn Tppppppp ppppppppp pppppppp pppppppp
+ // <------- (32 bits) node id -------> ^<------ (31 bits) pin id -------->
+ // | (1 bit) input (false) vs output (true)
+
+ // 1 is added to pin id to prevent the 0th node's 0th input pin resulting in a 0 global pin id
+ // (this is problematic because imgui-node-editor use 0 to represent null)
+
+ auto id = static_cast<uint64_t>(pinId);
+ GlobalPinId result;
+
+ result.Node = mNodes[id >> 32].get();
+ result.PinId = (uint32_t)(id & 0x000000001FFFFFFF) - 1;
+ result.IsOutput = id >> 31;
+
+ return result;
+}
+
+ImNodes::PinId Workflow::FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const
+{
+ // See this->DisassembleGlobalPinId for format details and rationale
+
+ uint64_t id = 0;
+ id |= ((uint64_t)node.GetId() << 32);
+ id |= (isOutput << 31);
+ id |= ((pinId + 1) & 0x1FFFFFFF);
+
+ return id;
+}
+
+const std::vector<std::vector<uint32_t>>& Workflow::GetDepthGroups() const
+{
+ return mDepthGroups;
+}
+
+bool Workflow::DoesDepthNeedsUpdate() const
+{
+ return mDepthsDirty;
+}
+
+void Workflow::AddNode(std::unique_ptr<WorkflowNode> step)
+{
+ auto [storage, id] = AllocWorkflowStep();
+ storage = std::move(step);
+ storage->OnAttach(*this, id);
+ storage->mWorkflow = this;
+ storage->mId = id;
+}
+
+void Workflow::RemoveNode(uint32_t id)
+{
+ auto& step = mNodes[id];
+ if (step == nullptr) return;
+
+ step->OnDetach();
+ step->mWorkflow = nullptr;
+ step->mId = WorkflowNode::kInvalidId;
+}
+
+void Workflow::RemoveConnection(uint32_t id)
+{
+ auto& conn = mConnections[id];
+ if (!conn.IsValid()) return;
+
+ mNodes[conn.SourceNode]->mInputs[conn.SourcePin].Connection = WorkflowNode::kInvalidId;
+ mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin].Connection = WorkflowNode::kInvalidId;
+
+ conn = {};
+ mDepthsDirty = true;
+}
+
+bool Workflow::Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin)
+{
+ auto& src = sourceNode.mOutputs[sourcePin];
+ auto& dst = destinationNode.mInputs[destinationPin];
+
+ // TODO report error to user?
+ if (src.GetMatchingType() != dst.GetMatchingType()) {
+ return false;
+ }
+
+ if (src.IsConnected()) {
+ DisconnectBySource(sourceNode, sourcePin);
+ }
+
+ auto [conn, id] = AllocWorkflowConnection();
+ conn.SourceNode = sourceNode.GetId();
+ conn.SourcePin = sourcePin;
+ conn.DestinationNode = destinationNode.GetId();
+ conn.DestinationPin = destinationPin;
+
+ src.Connection = id;
+ dst.Connection = id;
+
+ mDepthsDirty = true;
+ return true;
+}
+
+bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin)
+{
+ auto& sn = sourceNode.mOutputs[sourcePin];
+ if (!sn.IsConnected()) return false;
+
+ auto& conn = mConnections[sn.Connection];
+ auto& dn = mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin];
+
+ sn.Connection = WorkflowConnection::kInvalidId;
+ dn.Connection = WorkflowConnection::kInvalidId;
+ conn = {};
+
+ mDepthsDirty = true;
+ return true;
+}
+
+bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin)
+{
+ auto& dn = destinationNode.mOutputs[destinationPin];
+ if (!dn.IsConnected()) return false;
+
+ auto& conn = mConnections[dn.Connection];
+ auto& sn = mNodes[conn.SourceNode]->mInputs[conn.SourcePin];
+
+ sn.Connection = WorkflowConnection::kInvalidId;
+ dn.Connection = WorkflowConnection::kInvalidId;
+ conn = {};
+
+ mDepthsDirty = true;
+ return true;
+}
+
+Workflow::GraphUpdateResult Workflow::UpdateGraph(GraphUpdateDetails* details)
+{
+ if (!mDepthsDirty) {
+ return GUR_NoWorkToDo;
+ }
+
+ // Terminology:
+ // - Dependency = nodes its input pins are connected to
+ // - Dependents = nodes its output pins are connected to
+
+ struct WorkingNode
+ {
+ // The max depth out of all dependency nodes, maintained during the traversal and committed as the actual depth
+ // when all dependencies of this node has been resolved. Add 1 to get the depth that will be assigned to the node.
+ int MaximumDepth = 0;
+ int FulfilledInputCount = 0;
+ };
+
+ std::vector<WorkingNode> workingNodes;
+ std::queue<uint32_t> q;
+
+ // Check if all dependencies of this node is satisfied
+ auto CheckNodeDependencies = [&](WorkflowNode& node) -> bool {
+ for (auto& pin : node.mInputs) {
+ if (!pin.IsConnected()) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ workingNodes.reserve(mNodes.size());
+ {
+ std::vector<uint32_t> unsatisfiedNodes;
+ for (uint32_t i = 0; i < mNodes.size(); ++i) {
+ auto& node = mNodes[i];
+ workingNodes.push_back(WorkingNode{});
+
+ if (!node) continue;
+
+ if (!CheckNodeDependencies(*node)) {
+ unsatisfiedNodes.push_back(i);
+ }
+
+ node->mDepth = -1;
+
+ // Start traversing with the input nodes
+ if (node->GetType() == WorkflowNode::InputType) {
+ q.push(i);
+ }
+ }
+
+ if (!unsatisfiedNodes.empty()) {
+ if (details) {
+ details->emplace<decltype(unsatisfiedNodes)>(std::move(unsatisfiedNodes));
+ }
+ return GUR_UnsatisfiedDependencies;
+ }
+ }
+
+ auto ProcessNode = [&](WorkflowNode& node) -> void {
+ for (auto& pin : node.mOutputs) {
+ if (!pin.IsConnected()) continue;
+ auto& conn = mConnections[pin.Connection];
+
+ auto& wn = workingNodes[conn.DestinationNode];
+ auto& n = *mNodes[conn.DestinationPin].get();
+
+ wn.FulfilledInputCount++;
+ wn.MaximumDepth = std::max(node.mDepth, wn.MaximumDepth);
+
+ // Node's dependency is fulfilled, we can process its dependents next
+ // We use >= here because for a many-to-one pin, the dependency is an "or" relation ship, i.e. any of the nodes firing before this will fulfill the requirement
+ if (n.mInputs.size() >= wn.FulfilledInputCount) {
+ n.mDepth = wn.MaximumDepth + 1;
+ }
+ }
+ };
+
+ int processedNodes = 0;
+ while (!q.empty()) {
+ auto& wn = workingNodes[q.front()];
+ auto& n = *mNodes[q.front()];
+ q.pop();
+ processedNodes++;
+
+ ProcessNode(n);
+ }
+
+ if (processedNodes < mNodes.size()) {
+ // There is unreachable nodes, collect them and report to the caller
+
+ std::vector<uint32_t> unreachableNodes;
+ for (uint32_t i = 0; i < mNodes.size(); ++i) {
+ auto& wn = workingNodes[i];
+ auto& n = *mNodes[i];
+
+ // This is a reachable node
+ if (n.mDepth != -1) continue;
+
+ unreachableNodes.push_back(i);
+ }
+
+ if (details) {
+ details->emplace<decltype(unreachableNodes)>(std::move(unreachableNodes));
+ }
+ return GUR_UnreachableNodes;
+ }
+
+ return GUR_Success;
+}
+
+class Workflow::Private
+{
+public:
+ template <class TSelf, class TProxy>
+ static void OperateStream(TSelf& self, TProxy& proxy)
+ {
+ // TODO
+ }
+};
+
+void Workflow::ReadFromDataStream(InputDataStream& stream)
+{
+ Private::OperateStream(*this, stream);
+}
+
+void Workflow::WriteToDataStream(OutputDataStream& stream) const
+{
+ Private::OperateStream(*this, stream);
+}
+
+std::pair<WorkflowConnection&, uint32_t> Workflow::AllocWorkflowConnection()
+{
+ for (size_t idx = 0; idx < mConnections.size(); ++idx) {
+ auto& elm = mConnections[idx];
+ if (!elm.IsValid()) {
+ return { elm, (uint32_t)idx };
+ }
+ }
+
+ auto id = (uint32_t)mConnections.size();
+ auto& conn = mConnections.emplace_back(WorkflowConnection{});
+ conn.Id = id;
+
+ return { conn, id };
+}
+
+std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> Workflow::AllocWorkflowStep()
+{
+ for (size_t idx = 0; idx < mNodes.size(); ++idx) {
+ auto& elm = mNodes[idx];
+ if (elm == nullptr) {
+ return { elm, (uint32_t)idx };
+ }
+ }
+
+ auto id = (uint32_t)mNodes.size();
+ auto& node = mNodes.emplace_back(std::unique_ptr<WorkflowNode>());
+
+ return { node, id };
+}
+
+void WorkflowAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const
+{
+ auto dir = GetConnectedProject().GetWorkflowsDirectory();
+ DiscoverFilesByExtension(callback, dir, ".cplt-workflow"sv);
+}
+
+std::string WorkflowAssetList::RetrieveNameFromFile(const fs::path& file) const
+{
+ auto res = DataArchive::LoadFile(file);
+ if (!res) return "";
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ return assetInfo.Name;
+}
+
+uuids::uuid WorkflowAssetList::RetrieveUuidFromFile(const fs::path& file) const
+{
+ return uuids::uuid::from_string(file.stem().string());
+}
+
+fs::path WorkflowAssetList::RetrievePathFromAsset(const SavedAsset& asset) const
+{
+ auto fileName = uuids::to_string(asset.Uuid);
+ return GetConnectedProject().GetWorkflowPath(fileName);
+}
+
+bool WorkflowAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+ auto res = DataArchive::SaveFile(path);
+ if (!res) return false;
+ auto& stream = res.value();
+
+ stream.WriteObject(assetInfo);
+ // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Workflow
+ if (auto workflow = static_cast<const Workflow*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
+ stream.WriteObject(*workflow);
+ }
+
+ return true;
+}
+
+static std::unique_ptr<Workflow> LoadWorkflowFromFile(const fs::path& path)
+{
+ auto res = DataArchive::LoadFile(path);
+ if (!res) return nullptr;
+ auto& stream = res.value();
+
+ // TODO this is currently unused
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ auto workflow = std::make_unique<Workflow>();
+ stream.ReadObject(*workflow);
+
+ return workflow;
+}
+
+Workflow* WorkflowAssetList::LoadInstance(const SavedAsset& assetInfo) const
+{
+ return ::LoadWorkflowFromFile(RetrievePathFromAsset(assetInfo)).release();
+}
+
+Workflow* WorkflowAssetList::CreateInstance(const SavedAsset& assetInfo) const
+{
+ return new Workflow();
+}
+
+bool WorkflowAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+
+ auto workflow = ::LoadWorkflowFromFile(path);
+ if (!workflow) return false;
+
+ SaveInstance(assetInfo, workflow.get());
+
+ return true;
+}
+
+void WorkflowAssetList::DisplayAssetCreator(ListState& state)
+{
+ auto ValidateNewName = [&]() -> void {
+ if (mACNewName.empty()) {
+ mACNewNameError = NameSelectionError::Empty;
+ return;
+ }
+
+ if (FindByName(mACNewName)) {
+ mACNewNameError = NameSelectionError::Duplicated;
+ return;
+ }
+
+ mACNewNameError = NameSelectionError::None;
+ };
+
+ auto ShowNewNameErrors = [&]() -> void {
+ switch (mACNewNameError) {
+ case NameSelectionError::None: break;
+ case NameSelectionError::Duplicated:
+ ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR));
+ break;
+ case NameSelectionError::Empty:
+ ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR));
+ break;
+ }
+ };
+
+ auto IsInputValid = [&]() -> bool {
+ return mACNewNameError == NameSelectionError::None;
+ };
+
+ auto ResetState = [&]() -> void {
+ mACNewName.clear();
+ ValidateNewName();
+ };
+
+ if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) {
+ ValidateNewName();
+ }
+
+ ShowNewNameErrors();
+
+ if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) {
+ ImGui::CloseCurrentPopup();
+
+ Create(SavedAsset{
+ .Name = mACNewName,
+ });
+ ResetState();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void WorkflowAssetList::DisplayDetailsTable(ListState& state) const
+{
+ ImGui::BeginTable("AssetDetailsTable", 1, ImGuiTableFlags_Borders);
+
+ ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME));
+ ImGui::TableHeadersRow();
+
+ for (auto& asset : this->GetAssets()) {
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) {
+ state.SelectedAsset = &asset;
+ }
+ }
+
+ ImGui::EndTable();
+}
diff --git a/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp
new file mode 100644
index 0000000..ee3da28
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp
@@ -0,0 +1,143 @@
+#include "Workflow.hpp"
+
+#include <Cplt/Model/Workflow/Nodes/DocumentNodes.hpp>
+#include <Cplt/Model/Workflow/Nodes/NumericNodes.hpp>
+#include <Cplt/Model/Workflow/Nodes/TextNodes.hpp>
+#include <Cplt/Model/Workflow/Nodes/UserInputNodes.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/Macros.hpp>
+
+#include <memory>
+
+const char* WorkflowNode::FormatKind(Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition: return I18N_TEXT("Add", L10N_WORKFLOW_ADD);
+ case KD_NumericSubtraction: return I18N_TEXT("Subtract", L10N_WORKFLOW_SUB);
+ case KD_NumericMultiplication: return I18N_TEXT("Multiply", L10N_WORKFLOW_MUL);
+ case KD_NumericDivision: return I18N_TEXT("Divide", L10N_WORKFLOW_DIV);
+ case KD_NumericExpression: return I18N_TEXT("Evaluate expression", L10N_WORKFLOW_EVAL);
+ case KD_TextFormatting: return I18N_TEXT("Format text", L10N_WORKFLOW_FMT);
+ case KD_DocumentTemplateExpansion: return I18N_TEXT("Expand template", L10N_WORKFLOW_INSTANTIATE_TEMPLATE);
+ case KD_FormInput: return I18N_TEXT("Form input", L10N_WORKFLOW_FORM_INPUT);
+ case KD_DatabaseRowsInput: return I18N_TEXT("Database input", L10N_WORKFLOW_DB_INPUT);
+
+ case InvalidKind: break;
+ }
+ return "";
+}
+
+const char* WorkflowNode::FormatCategory(WorkflowNode::Category category)
+{
+ switch (category) {
+ case CG_Numeric: return I18N_TEXT("Numeric", L10N_WORKFLOW_CATEGORY_NUMERIC);
+ case CG_Text: return I18N_TEXT("Text", L10N_WORKFLOW_CATEGORY_TEXT);
+ case CG_Document: return I18N_TEXT("Document", L10N_WORKFLOW_CATEGORY_DOCUMENT);
+ case CG_UserInput: return I18N_TEXT("User input", L10N_WORKFLOW_CATEGORY_USER_INPUT);
+ case CG_SystemInput: return I18N_TEXT("System input", L10N_WORKFLOW_CATEGORY_SYS_INPUT);
+ case CG_Output: return I18N_TEXT("Output", L10N_WORKFLOW_CATEGORY_OUTPUT);
+
+ case InvalidCategory: break;
+ }
+ return "";
+}
+
+const char* WorkflowNode::FormatType(Type type)
+{
+ switch (type) {
+ case InputType: return I18N_TEXT("Input", L10N_WORKFLOW_KIND_INPUT);
+ case TransformType: return I18N_TEXT("Transform", L10N_WORKFLOW_KIND_TRANSFORM);
+ case OutputType: return I18N_TEXT("Output", L10N_WORKFLOW_KIND_OUTPUT);
+ }
+ return "";
+}
+
+WorkflowNode::Category WorkflowNode::QueryCategory(Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition:
+ case KD_NumericSubtraction:
+ case KD_NumericMultiplication:
+ case KD_NumericDivision:
+ case KD_NumericExpression:
+ return CG_Numeric;
+ case KD_TextFormatting:
+ return CG_Text;
+ case KD_DocumentTemplateExpansion:
+ return CG_Document;
+ case KD_FormInput:
+ case KD_DatabaseRowsInput:
+ return CG_UserInput;
+
+ case InvalidKind: break;
+ }
+ return InvalidCategory;
+}
+
+std::span<const WorkflowNode::Kind> WorkflowNode::QueryCategoryMembers(Category category)
+{
+ constexpr WorkflowNode::Kind kNumeric[] = {
+ KD_NumericAddition,
+ KD_NumericSubtraction,
+ KD_NumericMultiplication,
+ KD_NumericDivision,
+ KD_NumericExpression,
+ };
+
+ constexpr WorkflowNode::Kind kText[] = {
+ KD_TextFormatting,
+ };
+
+ constexpr WorkflowNode::Kind kDocument[] = {
+ KD_DocumentTemplateExpansion,
+ };
+
+ constexpr WorkflowNode::Kind kUserInput[] = {
+ KD_FormInput,
+ KD_DatabaseRowsInput,
+ };
+
+ // TODO remove invalid kinds after we have nodes of these categories
+ constexpr WorkflowNode::Kind kSystemInput[] = {
+ InvalidKind,
+ };
+
+ constexpr WorkflowNode::Kind kOutput[] = {
+ InvalidKind,
+ };
+
+ switch (category) {
+ case CG_Numeric: return kNumeric;
+ case CG_Text: return kText;
+ case CG_Document: return kDocument;
+ case CG_UserInput: return kUserInput;
+ case CG_SystemInput: return kSystemInput;
+ case CG_Output: return kOutput;
+
+ case InvalidCategory: break;
+ }
+ return {};
+}
+
+std::unique_ptr<WorkflowNode> WorkflowNode::CreateByKind(WorkflowNode::Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition: return std::make_unique<NumericOperationNode>(NumericOperationNode::Addition);
+ case KD_NumericSubtraction: return std::make_unique<NumericOperationNode>(NumericOperationNode::Subtraction);
+ case KD_NumericMultiplication: return std::make_unique<NumericOperationNode>(NumericOperationNode::Multiplication);
+ case KD_NumericDivision: return std::make_unique<NumericOperationNode>(NumericOperationNode::Division);
+ case KD_NumericExpression: return std::make_unique<NumericExpressionNode>();
+ case KD_TextFormatting: return std::make_unique<TextFormatterNode>();
+ case KD_DocumentTemplateExpansion: return std::make_unique<DocumentTemplateExpansionNode>();
+ case KD_FormInput: return std::make_unique<FormInputNode>();
+ case KD_DatabaseRowsInput: return std::make_unique<DatabaseRowsInputNode>();
+
+ case InvalidKind: break;
+ }
+ return nullptr;
+}
+
+bool WorkflowNode::IsInstance(const WorkflowNode* node)
+{
+ return true;
+}
diff --git a/app/source/Cplt/Model/Workflow/fwd.hpp b/app/source/Cplt/Model/Workflow/fwd.hpp
new file mode 100644
index 0000000..ce5b6db
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/fwd.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Nodes/fwd.hpp>
+#include <Cplt/Model/Workflow/Values/fwd.hpp>
+
+// Evaluation.hpp
+class WorkflowEvaluationError;
+class WorkflowEvaluationContext;
+
+// SavedWorkflow.hpp
+class SavedWorkflowCache;
+class SavedWorkflow;
+
+// Value.hpp
+class BaseValue;
+class BaseObjectValue;
+
+// Workflow.hpp
+class WorkflowConnection;
+class WorkflowNode;
+class Workflow;
+class WorkflowAssetList;