From 7fe47a9d5b1727a61dc724523b530762f6d6ba19 Mon Sep 17 00:00:00 2001 From: rtk0c Date: Thu, 30 Jun 2022 21:38:53 -0700 Subject: Restructure project --- app/source/Cplt/Model/Workflow/Workflow_Main.cpp | 846 +++++++++++++++++++++++ 1 file changed, 846 insertions(+) create mode 100644 app/source/Cplt/Model/Workflow/Workflow_Main.cpp (limited to 'app/source/Cplt/Model/Workflow/Workflow_Main.cpp') 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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& Workflow::GetConnections() const +{ + return mConnections; +} + +std::vector& Workflow::GetConnections() +{ + return mConnections; +} + +const std::vector>& Workflow::GetNodes() const +{ + return mNodes; +} + +std::vector>& Workflow::GetNodes() +{ + return mNodes; +} + +const std::vector>& Workflow::GetConstants() const +{ + return mConstants; +} + +std::vector>& 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(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>& Workflow::GetDepthGroups() const +{ + return mDepthGroups; +} + +bool Workflow::DoesDepthNeedsUpdate() const +{ + return mDepthsDirty; +} + +void Workflow::AddNode(std::unique_ptr 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 workingNodes; + std::queue 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 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(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 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(std::move(unreachableNodes)); + } + return GUR_UnreachableNodes; + } + + return GUR_Success; +} + +class Workflow::Private +{ +public: + template + 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 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&, 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()); + + return { node, id }; +} + +void WorkflowAssetList::DiscoverFiles(const std::function& 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, which will ensure `asset` points to some Workflow + if (auto workflow = static_cast(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + stream.WriteObject(*workflow); + } + + return true; +} + +static std::unique_ptr 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(); + 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(); +} -- cgit v1.2.3-70-g09d2