diff options
Diffstat (limited to 'app/source/Cplt/Model/Workflow')
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; |