#include "Workflow.hpp" #include #include #include #include #include WorkflowConnection::WorkflowConnection() : MultiConnections{} , SingleConnection{ WorkflowNode::kInvalidId, -1 } , ConnectionDirection{ OneToMany } { } bool WorkflowConnection::IsValid() const { return SingleConnection.Node != WorkflowNode::kInvalidId; } std::span WorkflowConnection::GetSourcePoints() { switch (ConnectionDirection) { case ManyToOne: return MultiConnections; case OneToMany: return { &SingleConnection, 1 }; } } std::span WorkflowConnection::GetSourcePoints() const { switch (ConnectionDirection) { case ManyToOne: return MultiConnections; case OneToMany: return { &SingleConnection, 1 }; } } std::span WorkflowConnection::GetDestinationPoints() { switch (ConnectionDirection) { case ManyToOne: return { &SingleConnection, 1 }; case OneToMany: return MultiConnections; } } std::span WorkflowConnection::GetDestinationPoints() const { switch (ConnectionDirection) { case ManyToOne: return { &SingleConnection, 1 }; case OneToMany: return MultiConnections; } } 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; } WorkflowConnection::Direction WorkflowNode::InputPin::GetSupportedDirection() const { return AllowsMultipleConnections ? WorkflowConnection::ManyToOne : WorkflowConnection::OneToMany; } bool WorkflowNode::OutputPin::IsConnected() const { return Connection != WorkflowConnection::kInvalidId; } BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const { return MatchingType; } WorkflowConnection::Direction WorkflowNode::OutputPin::GetSupportedDirection() const { return AllowsMultipleConnections ? WorkflowConnection::OneToMany : WorkflowConnection::ManyToOne; } WorkflowNode::WorkflowNode(Type type, Kind kind) : mType{ type } , mKind{ kind } , mDepth{ -1 } { } size_t WorkflowNode::GetId() const { return mId; } WorkflowNode::Type WorkflowNode::GetType() const { return mType; } WorkflowNode::Kind WorkflowNode::GetKind() const { return mKind; } void WorkflowNode::ConnectInput(int nodeId, WorkflowNode& output, int outputNodeId) { mWorkflow->Connect(*this, nodeId, output, outputNodeId); } void WorkflowNode::DisconnectInput(int nodeId) { mWorkflow->DisconnectByDestination(*this, nodeId); } bool WorkflowNode::IsInputConnected(int nodeId) const { return mInputs[nodeId].IsConnected(); } void WorkflowNode::ConnectOutput(int nodeId, WorkflowNode& input, int inputNodeId) { mWorkflow->Connect(input, inputNodeId, *this, nodeId); } void WorkflowNode::DisconnectOutput(int nodeId) { mWorkflow->DisconnectBySource(*this, nodeId); } bool WorkflowNode::IsOutputConnected(int nodeId) const { return mOutputs[nodeId].IsConnected(); } 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) { // Delay assignment to ConnectionPoint::Pin so that the pinB loop (if it happens to run and looks in the same Connection) doesn't match the point updated in the pinA loop using Pt = WorkflowConnection::ConnectionPoint; Pt* pointA = nullptr; Pt* pointB = nullptr; if (pinA.IsConnected() && !pinA.IsConstantConnection()) { auto pts = mWorkflow->GetConnectionById(pinA.Connection)->GetDestinationPoints(); auto it = std::find(pts.begin(), pts.end(), Pt{ mId, a }); pointA = it == pts.end() ? nullptr : &*it; } if (pinB.IsConnected() && !pinB.IsConstantConnection()) { auto pts = mWorkflow->GetConnectionById(pinB.Connection)->GetDestinationPoints(); auto it = std::find(pts.begin(), pts.end(), Pt{ mId, b }); pointB = it == pts.end() ? nullptr : &*it; } if (pointA) pointA->Pin = b; if (pointB) pointB->Pin = 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) { using Pt = WorkflowConnection::ConnectionPoint; Pt* pointA = nullptr; Pt* pointB = nullptr; if (pinA.IsConnected()) { auto pts = mWorkflow->GetConnectionById(pinA.Connection)->GetSourcePoints(); auto it = std::find(pts.begin(), pts.end(), Pt{ mId, a }); pointA = it == pts.end() ? nullptr : &*it; } if (pinB.IsConnected()) { auto pts = mWorkflow->GetConnectionById(pinB.Connection)->GetSourcePoints(); auto it = std::find(pts.begin(), pts.end(), Pt{ mId, b }); pointB = it == pts.end() ? nullptr : &*it; } if (pointA) pointA->Pin = b; if (pointB) pointB->Pin = a; } std::swap(pinA, pinB); } WorkflowConnection* Workflow::GetConnectionById(size_t id) { return &mConnections[id]; } WorkflowNode* Workflow::GetStepById(size_t id) { return mNodes[id].get(); } BaseValue* Workflow::GetConstantById(size_t id) { return mConstants[id].get(); } void Workflow::AddStep(std::unique_ptr step) { auto [storage, id] = AllocWorkflowStep(); storage = std::move(step); storage->OnAttach(*this, id); storage->mWorkflow = this; storage->mId = id; } void Workflow::RemoveStep(size_t id) { auto& step = mNodes[id]; if (step == nullptr) return; step->OnDetach(); step->mWorkflow = nullptr; step->mId = WorkflowNode::kInvalidId; } bool Workflow::Connect(WorkflowNode& sourceNode, int sourcePin, WorkflowNode& destinationNode, int destinationPin) { auto& src = sourceNode.mOutputs[sourcePin]; auto& dst = destinationNode.mInputs[destinationPin]; // Equivalent to `if (o.GetSupportedDirection() != i.GetSupportedDirection()) return false;` // Cannot connect two pins of different type if (src.AllowsMultipleConnections == dst.AllowsMultipleConnections) return false; // Would be same as `dst.GetSupportedDirection()` because we validated that they are the same above auto direction = src.GetSupportedDirection(); // TODO report error to user? if (src.GetMatchingType() != dst.GetMatchingType()) { return false; } using Pt = WorkflowConnection::ConnectionPoint; Pt* srcConnPt; Pt* dstConnPt; size_t connId; switch (direction) { case WorkflowConnection::ManyToOne: { if (dst.IsConnected()) return false; WorkflowConnection* conn; if (src.IsConnected()) { conn = &mConnections[src.Connection]; connId = src.Connection; } else { auto p = AllocWorkflowConnection(); conn = &p.first; connId = p.second; } srcConnPt = &conn->MultiConnections.emplace_back(); dstConnPt = &conn->SingleConnection; } break; case WorkflowConnection::OneToMany: { if (src.IsConnected()) return false; WorkflowConnection* conn; if (dst.IsConnected()) { conn = &mConnections[src.Connection]; connId = src.Connection; } else { auto p = AllocWorkflowConnection(); conn = &p.first; connId = p.second; } srcConnPt = &conn->SingleConnection; dstConnPt = &conn->MultiConnections.emplace_back(); } break; } srcConnPt->Node = sourceNode.GetId(); srcConnPt->Pin = sourcePin; dstConnPt->Node = destinationNode.GetId(); dstConnPt->Pin = destinationPin; src.Connection = connId; dst.Connection = connId; mDepthsDirty = true; return true; } bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, int sourcePin) { auto& sp = sourceNode.mOutputs[sourcePin]; if (!sp.IsConnected()) return false; auto& conn = mConnections[sp.Connection]; using Pt = WorkflowConnection::ConnectionPoint; switch (sp.GetSupportedDirection()) { case WorkflowConnection::ManyToOne: { // Recessive pin, remove ConnectionPoint associated with this pin only auto& vec = conn.MultiConnections; vec.erase(std::remove(vec.begin(), vec.end(), Pt{ sourceNode.GetId(), sourcePin }), vec.end()); sp.Connection = WorkflowConnection::kInvalidId; } break; case WorkflowConnection::OneToMany: { // Dominate pin, removes whole connection for (auto& pt : conn.MultiConnections) { auto& node = *mNodes[pt.Node]; node.mInputs[pt.Pin].Connection = WorkflowConnection::kInvalidId; } sp.Connection = WorkflowConnection::kInvalidId; conn = {}; } break; } mDepthsDirty = true; return true; } bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, int destinationPin) { auto& dp = destinationNode.mInputs[destinationPin]; if (!dp.IsConnected()) return false; if (dp.IsConstantConnection()) { dp.ConnectionToConst = false; dp.Connection = WorkflowConnection::kInvalidId; return true; } auto& conn = mConnections[dp.Connection]; using Pt = WorkflowConnection::ConnectionPoint; switch (dp.GetSupportedDirection()) { case WorkflowConnection::ManyToOne: { // Dominate pin, removes whole connection for (auto& pt : conn.MultiConnections) { auto& node = *mNodes[pt.Node]; node.mOutputs[pt.Pin].Connection = WorkflowConnection::kInvalidId; } dp.Connection = WorkflowConnection::kInvalidId; conn = {}; } break; case WorkflowConnection::OneToMany: { // Recessive pin, remove ConnectionPoint associated with this pin only auto& vec = conn.MultiConnections; vec.erase(std::remove(vec.begin(), vec.end(), Pt{ destinationNode.GetId(), destinationPin }), vec.end()); dp.Connection = WorkflowConnection::kInvalidId; } break; } mDepthsDirty = true; return true; } void Workflow::RemoveConnection(size_t id) { auto& conn = mConnections[id]; if (!conn.IsValid()) return; switch (conn.ConnectionDirection) { case WorkflowConnection::ManyToOne: { } break; case WorkflowConnection::OneToMany: { } break; } // TODO mDepthsDirty = true; } bool Workflow::DoesDepthNeedsUpdate() const { return mDepthsDirty; } Workflow::GraphUpdateResult Workflow::UpdateGraph() { // Terminology: // - Input pin <=> dependency nodes 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 int MaximumDepth = 0; int FulfilledInputCount = 0; }; std::vector workingNodes; tsl::robin_set workingSet; // The set of valid (known to has depth) nodes, used for iterating tsl::robin_set backWorkingSet; // The set of valid nodes built while iterating `workingSet`, and will be moved into it after done iterating workingNodes.reserve(mNodes.size()); for (size_t i = 0; i < mNodes.size(); ++i) { auto& node = mNodes[i]; workingNodes.push_back(WorkingNode{}); if (!node) continue; node->mDepth = -1; // Start traversing with the input nodes if (node->GetType() == WorkflowNode::InputType) { workingSet.insert(i); } } auto HasCyclicReference = [&](WorkflowNode* node) -> bool { // TODO return false; }; auto AddOutputsToWorkingSet = [&](WorkflowNode* node) -> void { for (auto& pin : node->mOutputs) { if (!pin.IsConnected()) continue; auto& conn = mConnections[pin.Connection]; for (auto& point : conn.GetDestinationPoints()) { auto& wn = workingNodes[point.Node]; auto& n = *mNodes[point.Node].get(); wn.FulfilledInputCount++; if (HasCyclicReference(n)) { // TODO break; } // Fulfilled node // 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 // TODO calc depth based on previous dependencies if (n.mInputs.size() >= wn.FulfilledInputCount) { backWorkingSet.insert(point.Node); } } } }; int processedNodes = 0; int currentDepth = 0; while (true) { for (size_t idx : workingSet) { auto& wn = workingNodes[idx]; auto& n = *mNodes[idx]; if (n.mInputs.size() == wn.FulfilledInputCount) { n.mDepth = currentDepth; AddOutputsToWorkingSet(n); processedNodes++; } } workingSet = std::move(backWorkingSet); backWorkingSet.clear(); currentDepth++; if (processedNodes == mNodes.size()) { break; } } return GraphUpdateResult::Success; } std::pair Workflow::AllocWorkflowConnection() { for (size_t idx = 0; idx < mConnections.size(); ++idx) { auto& elm = mConnections[idx]; if (!elm.IsValid()) { return { elm, idx }; } } auto id = mConnections.size(); return { mConnections.emplace_back(WorkflowConnection{}), id }; } std::pair&, size_t> Workflow::AllocWorkflowStep() { for (size_t idx = 0; idx < mNodes.size(); ++idx) { auto& elm = mNodes[idx]; if (elm == nullptr) { return { elm, idx }; } } auto id = mNodes.size(); return { mNodes.emplace_back(std::unique_ptr()), id }; }