#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(); }