From 9f2891515212db3c02d387b987f826d16bf62378 Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Thu, 17 Apr 2025 17:08:54 -0700 Subject: [PATCH 1/6] wip red edge --- .../StitchEngine/Actions/GraphCalculate.swift | 88 ++++++++++----- Sources/StitchEngine/Actions/HoseFlow.swift | 105 +++++++++++------- .../Actions/TopologicalDataRefresh.swift | 15 +++ .../StitchEngine/Data/GraphCalculatable.swift | 8 +- Sources/StitchEngine/GraphQueueLogic.swift | 55 +++++++-- Sources/StitchEngine/TopologicalData.swift | 23 +++- 6 files changed, 213 insertions(+), 81 deletions(-) diff --git a/Sources/StitchEngine/Actions/GraphCalculate.swift b/Sources/StitchEngine/Actions/GraphCalculate.swift index 765a717..23aa6c0 100644 --- a/Sources/StitchEngine/Actions/GraphCalculate.swift +++ b/Sources/StitchEngine/Actions/GraphCalculate.swift @@ -16,46 +16,23 @@ extension GraphCalculatable { Set>, // portsToUpdate Bool // shouldResortPreviewLayers ) { - let graphState = self - - var visitedNodes = Set() - var queue = nodeIds - - // Reset state on scheduled cycle nodes - self.topologicalData.nodesForNextGraphStep = .init() - - var portsToUpdate = Set>() + let redEdgeCycles = self.topologicalData.cycleNodesForNextGraphStep - while let nodeId = topologicalData.getNextNodeToCalculate(for: queue, - visitedNodes: visitedNodes) { - queue.remove(nodeId) - - guard !visitedNodes.contains(nodeId) else { - assertInDebug(self.topologicalData.cycleContains(nodeId)) - -#if DEV_DEBUG - // print("GraphState.calculate: scheduling cycle node \(nodeId)") -#endif - - self.topologicalData.nodesForNextGraphStep.insert(nodeId) - continue - } - - visitedNodes.insert(nodeId) - + return self.traverseGraph(from: nodeIds, + redEdgeCycles: redEdgeCycles) { queuedNodeResult, queue in // Retrieve the node afresh everytime, // since an upstream node's changes may have changed its inputs - guard let node = self.getNode(id: nodeId) else { + guard let node = self.getNode(id: queuedNodeResult.nodeId) else { // Not necessarily bad -- can happen if we deleted nodes but those nodes were still scheduled to run. // fatalErrorIfDebug() - continue + return } let existingOutputValues = node.outputsValuesList let outputCoordinates = node.outputCoordinates guard let evalResult = self.calculateNode(node) else { - continue + return } // Update queue with NodeIds for nodes which need re-evaluation @@ -71,9 +48,60 @@ extension GraphCalculatable { // Update queue set with changed downstream nodes queue = changedNodeIds.union(Set(queue)) + } + } + + @MainActor + /// The main graph calculator. Shouldn't be called directly unless you know what you're doing. + public func traverseGraph( + from nodeIds: Set, + redEdgeCycles: Set>, + callback: @escaping @MainActor (QueuedNodeResultType, inout Set) -> () + ) -> ( + Set>, // portsToUpdate + Bool // shouldResortPreviewLayers + ) { + let graphState = self + + var visitedNodes = Set() + var queue = nodeIds + + // Reset state on scheduled cycle nodes + self.topologicalData.nodesForNextGraphStep = .init() + self.topologicalData.cycleNodesForNextGraphStep = .init() + + var portsToUpdate = Set>() + + // First handle red edge cycle nodes from last graph step by update inputs of nodes + for cycleNodeData in redEdgeCycles { + let _ = self.updateInputs(inputCoordinate: cycleNodeData.portId, + upstreamOutputValues: cycleNodeData.values, + mediaList: cycleNodeData.mediaList, + upstreamOutputChanged: true) + } + + while let queuedNodeResult = topologicalData.getNextNodeToCalculate(for: queue, + visitedNodes: visitedNodes) { + let nodeId = queuedNodeResult.nodeId + queue.remove(nodeId) + + guard !visitedNodes.contains(nodeId) else { + assertInDebug(self.topologicalData.cycleContains(nodeId)) + +#if DEV_DEBUG + // print("GraphState.calculate: scheduling cycle node \(nodeId)") +#endif + + self.topologicalData.nodesForNextGraphStep.insert(nodeId) + continue + } + + visitedNodes.insert(nodeId) + + callback(queuedNodeResult, &queue) // Track changed outputs here, inputs in didInputsUpdate - portsToUpdate.insert(NodePortType.allOutputs(node.id)) + portsToUpdate.insert(NodePortType.allOutputs(nodeId)) } // while let ... diff --git a/Sources/StitchEngine/Actions/HoseFlow.swift b/Sources/StitchEngine/Actions/HoseFlow.swift index d4f2539..6482139 100644 --- a/Sources/StitchEngine/Actions/HoseFlow.swift +++ b/Sources/StitchEngine/Actions/HoseFlow.swift @@ -68,48 +68,77 @@ extension GraphCalculatable { var changedIds = Set() for inputCoordinate in inputs { - // Get kind of downstream node - if let nodeViewModel = self.getNode(id: inputCoordinate.nodeId), - let inputObserver = nodeViewModel.getInputRowObserver(for: inputCoordinate.portType) { - let inputOldValues = inputObserver.values + // Catch cycle red edge case + if self.topologicalData.cycleStartingPoints.contains(inputCoordinate.nodeId) { + self.topologicalData.cycleNodesForNextGraphStep.insert( + .init(portId: inputCoordinate, + values: upstreamOutputValues, + mediaList: mediaList) + ) - if inputObserver.isPulseNodeType && !upstreamOutputChanged { - // If this is a pulse type input and the upstream output did not change, - // do not set the flowing value into the input. - // (Truthy values are coerced to current graph time, i.e. a pulse; we can only pulse when values actually change.) - continue - // } else if upstreamOutputChanged { - } else { - - guard let existingInputValue = inputOldValues.first else { - continue - } - - // `updateInputs(incomingValues: PortValues, graphTime) -> Bool` - // if we true, then - - // Note: if the input supports directly copying, then these values will not actually be coerced - let flowValuesCoercedToThisInputType = inputObserver.coerce( - theseValues: upstreamOutputValues, - toThisType: existingInputValue, - currentGraphTime: self.currentGraphTime) - - if inputOldValues != flowValuesCoercedToThisInputType { - - inputObserver.setValuesInInput(flowValuesCoercedToThisInputType) - changedIds.insert(inputObserver.id) - - // Update downstream observers - if let mediaList = mediaList, - let mediaObservers = nodeViewModel.getMediaObservers(port: inputCoordinate) { - nodeViewModel.updateInputMedia(inputCoordinate: inputCoordinate, - mediaList: mediaList) - } - } - } + continue } + + let newChangedIds = self.updateInputs(inputCoordinate: inputCoordinate, + upstreamOutputValues: upstreamOutputValues, + mediaList: mediaList, + upstreamOutputChanged: upstreamOutputChanged) + + changedIds = changedIds.union(newChangedIds) } // for inputCoordinate in ... return changedIds } + + @MainActor + func updateInputs(inputCoordinate: Self.Node.InputRow.RowID, + upstreamOutputValues: [Self.Node.PortData], + mediaList: [Self.Node.EvalResult.MediaType?]?, + upstreamOutputChanged: Bool) -> Set { + // Get kind of downstream node + guard let nodeViewModel = self.getNode(id: inputCoordinate.nodeId), + let inputObserver = nodeViewModel.getInputRowObserver(for: inputCoordinate.portType) else { + return .init() + } + + var changedIds = Set() + let inputOldValues = inputObserver.values + + if inputObserver.isPulseNodeType && !upstreamOutputChanged { + // If this is a pulse type input and the upstream output did not change, + // do not set the flowing value into the input. + // (Truthy values are coerced to current graph time, i.e. a pulse; we can only pulse when values actually change.) + return .init() + // } else if upstreamOutputChanged { + } else { + + guard let existingInputValue = inputOldValues.first else { + return .init() + } + + // `updateInputs(incomingValues: PortValues, graphTime) -> Bool` + // if we true, then + + // Note: if the input supports directly copying, then these values will not actually be coerced + let flowValuesCoercedToThisInputType = inputObserver.coerce( + theseValues: upstreamOutputValues, + toThisType: existingInputValue, + currentGraphTime: self.currentGraphTime) + + if inputOldValues != flowValuesCoercedToThisInputType { + + inputObserver.setValuesInInput(flowValuesCoercedToThisInputType) + changedIds.insert(inputObserver.id) + + // Update downstream observers + if let mediaList = mediaList, + let mediaObservers = nodeViewModel.getMediaObservers(port: inputCoordinate) { + nodeViewModel.updateInputMedia(inputCoordinate: inputCoordinate, + mediaList: mediaList) + } + } + } + + return changedIds + } } diff --git a/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift b/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift index a42034e..b9b1fef 100644 --- a/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift +++ b/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift @@ -17,6 +17,7 @@ extension GraphCalculatable { // All nodes excludes group nodes let allNodes = Array(self.nodes.values.filter { !$0.isGroupNode }) + let allNodeIds: Set = Set(allNodes.map(\.id)) let connections = self.createConnections() // Maps connections by node instead of by coordinate @@ -47,5 +48,19 @@ extension GraphCalculatable { self.topologicalData._allMustRunNodes = self.topologicalData.nodesToAlwaysRun .union(self.topologicalData.nodesScheduledToRun) .union(self.topologicalData.keyboardNodes) + + // Traverses full graph to identify cycle nodes that need to establish a red edge + var cycleStartingPoints = Set() + let _ = self.traverseGraph(from: allNodeIds, + // can be empty as we're just trying to traverse + redEdgeCycles: .init()) { queuedNodeResult, queue in + switch queuedNodeResult { + case .cycle(let nextCycleNodeResult): + cycleStartingPoints.insert(nextCycleNodeResult.nodeId) + + default: + return + } + } } } diff --git a/Sources/StitchEngine/Data/GraphCalculatable.swift b/Sources/StitchEngine/Data/GraphCalculatable.swift index e2954e9..739fe3b 100644 --- a/Sources/StitchEngine/Data/GraphCalculatable.swift +++ b/Sources/StitchEngine/Data/GraphCalculatable.swift @@ -75,9 +75,11 @@ extension GraphCalculatable { } @MainActor public var nodesToRunOnGraphStep: Set { - get { - self.topologicalData.nodesToRunOnGraphStep - } + self.topologicalData.nodesToRunOnGraphStep + } + + @MainActor public var hasUnprocessedCycleNodes: Bool { + !self.topologicalData.cycleNodesForNextGraphStep.isEmpty } @MainActor public func setNodesForNextGraphStep(_ nodeIds: Set) { diff --git a/Sources/StitchEngine/GraphQueueLogic.swift b/Sources/StitchEngine/GraphQueueLogic.swift index f26f9e3..bcab093 100644 --- a/Sources/StitchEngine/GraphQueueLogic.swift +++ b/Sources/StitchEngine/GraphQueueLogic.swift @@ -24,7 +24,7 @@ extension GraphTopologicalData { } @MainActor func getNextNodeToCalculate(for nodeIds: Set, - visitedNodes: Set) -> Node.ID? { + visitedNodes: Set) -> QueuedNodeResultType? { if let nextNodeId = self.memoizedQueues[nodeIds] { return nextNodeId } @@ -44,7 +44,7 @@ extension GraphTopologicalData { /// from the other nodes. Therefore we can pick any node matching that criteria. /// The only exception is if the remaining nodes contain a cycle at its root. @MainActor private func findNextNodeInGraphCalc(queuedNodeIds: Set, - visitedNodes: Set) -> Node.ID? { + visitedNodes: Set) -> QueuedNodeResultType? { if queuedNodeIds.isEmpty { // print("TopologicalData.findNextNodeInGraphCalc: none") return nil @@ -54,15 +54,17 @@ extension GraphTopologicalData { // preventing us from finding a natural root node. guard let nextNode = self.findNextNodeInDAG(queuedNodeIds: queuedNodeIds, visitedNodes: visitedNodes) else { - let cycleNode = self.findNextNodeInCycle(queuedNodeIds: queuedNodeIds, - visitedNodes: visitedNodes) + guard let cycleNode = self.findNextNodeInCycle(queuedNodeIds: queuedNodeIds, + visitedNodes: visitedNodes) else { + return nil + } // print("TopologicalData.findNextNodeInGraphCalc: cycle node \(cycleNode)") - return cycleNode + return .cycle(cycleNode) } // print("TopologicalData.findNextNodeInGraphCalc: non-cycle node \(nextNode)") - return nextNode + return .noncycle(nextNode) } @MainActor private func findNextNodeInDAG(queuedNodeIds: Set, @@ -87,7 +89,7 @@ extension GraphTopologicalData { /// 2. Tiebreakers resort to leveraging nodes in a cycle containing a direct connection to a node that was already visited /// in a graph. This ensures cycle calculation starts at the "red edge". @MainActor private func findNextNodeInCycle(queuedNodeIds: Set, - visitedNodes: Set) -> Node.ID? { + visitedNodes: Set) -> NextCycleNodeResult? { // There are cycle nodes involved if the above condition isn't hit. We'll prioritize // the cycle node with the fewest matching upstream parents to other nodes in the set. // Doing so guarantees the upstream-most cycle will be computed first. @@ -108,6 +110,16 @@ extension GraphTopologicalData { // Another sorting to guarantee consistent results .sorted() + let upstreamNodeCounts = sortedCycleNodes.map { + self.getAllUpstreamNodes(from: $0).intersection(queuedNodeIds).count + } + + guard let smallestCount = upstreamNodeCounts.first else { + return nil + } + + let hasConflict = upstreamNodeCounts.filter { $0 == smallestCount }.count > 1 + // Prioritize nodes in cycle which contain direct upstream parent to visited node // in this cycle (aka the "red edge") let cycleNode = sortedCycleNodes @@ -121,7 +133,32 @@ extension GraphTopologicalData { // print("TopologicalData.findNextNodeInGraphCalc: cycle node detected at \(cycleNode)") - return cycleNode + guard let cycleNode = cycleNode else { + return nil + } + + return .init(nodeId: cycleNode, + hasConflict: hasConflict) } - +} + +public enum QueuedNodeResultType { + case noncycle(Node.ID) + case cycle(NextCycleNodeResult) +} + +extension QueuedNodeResultType { + var nodeId: Node.ID { + switch self { + case .noncycle(let id): + return id + case .cycle(let nextCycleNodeResult): + return nextCycleNodeResult.nodeId + } + } +} + +public struct NextCycleNodeResult { + let nodeId: Node.ID + let hasConflict: Bool } diff --git a/Sources/StitchEngine/TopologicalData.swift b/Sources/StitchEngine/TopologicalData.swift index bd33370..dff1a68 100644 --- a/Sources/StitchEngine/TopologicalData.swift +++ b/Sources/StitchEngine/TopologicalData.swift @@ -7,6 +7,23 @@ import Foundation +public struct CycleNodeUpdateData { + let portId: Node.InputRow.RowID + let values: [Node.PortData] + let mediaList: [Node.EvalResult.MediaType?]? +} + +extension CycleNodeUpdateData: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.nodeId) + hasher.combine(self.portId) + } + + var nodeId: Node.ID { + self.portId.nodeId + } +} + public final class GraphTopologicalData: Sendable { public typealias InputRowId = Node.InputRow.RowID public typealias OutputRowId = Node.OutputRow.RowID @@ -23,6 +40,8 @@ public final class GraphTopologicalData: Sendable { @MainActor var nodesForNextGraphStep = Set() + @MainActor var cycleNodesForNextGraphStep = Set>() + // Maps NodeID to set of connected input coordinates @MainActor var shallowDownstreamNodes = ShallowDownstreamNodesDict() @@ -33,10 +52,12 @@ public final class GraphTopologicalData: Sendable { @MainActor var connections = Connections() + @MainActor var cycleStartingPoints = Set() + @MainActor var _allMustRunNodes = Set() // Memoizes a sorted list of nodes to eval given some set of node IDs - @MainActor var memoizedQueues = [Set: Node.ID]() + @MainActor var memoizedQueues = [Set: QueuedNodeResultType]() // Nodes that scheduled themselves via their node eval, // e.g. Classic Animation node From 3f0ceeb5288501143a5013120da0a64deb045f1b Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Fri, 18 Apr 2025 15:26:06 -0700 Subject: [PATCH 2/6] assign cycleStartingPoints --- Sources/StitchEngine/Actions/TopologicalDataRefresh.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift b/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift index b9b1fef..602a19f 100644 --- a/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift +++ b/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift @@ -62,5 +62,7 @@ extension GraphCalculatable { return } } + + self.topologicalData.cycleStartingPoints = cycleStartingPoints } } From b9a9aeabfc64675c1022b45d9c9edceaa64b3214 Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Fri, 18 Apr 2025 16:01:21 -0700 Subject: [PATCH 3/6] fix --- .../StitchEngine/Actions/GraphCalculate.swift | 15 +++-- Sources/StitchEngine/Actions/HoseFlow.swift | 55 +++++++++++-------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/Sources/StitchEngine/Actions/GraphCalculate.swift b/Sources/StitchEngine/Actions/GraphCalculate.swift index 23aa6c0..ec4ff4d 100644 --- a/Sources/StitchEngine/Actions/GraphCalculate.swift +++ b/Sources/StitchEngine/Actions/GraphCalculate.swift @@ -74,10 +74,17 @@ extension GraphCalculatable { // First handle red edge cycle nodes from last graph step by update inputs of nodes for cycleNodeData in redEdgeCycles { - let _ = self.updateInputs(inputCoordinate: cycleNodeData.portId, - upstreamOutputValues: cycleNodeData.values, - mediaList: cycleNodeData.mediaList, - upstreamOutputChanged: true) + let didChangeInputs = self.updateInputs(inputCoordinate: cycleNodeData.portId, + upstreamOutputValues: cycleNodeData.values, + mediaList: cycleNodeData.mediaList, + upstreamOutputChanged: true, + // empty cycle starting points ensures we update inputs + cycleStartingPoints: .init()) + + // Run red edge cycle node's eval if inputs changed from this update + if didChangeInputs { + queue.insert(cycleNodeData.nodeId) + } } while let queuedNodeResult = topologicalData.getNextNodeToCalculate(for: queue, diff --git a/Sources/StitchEngine/Actions/HoseFlow.swift b/Sources/StitchEngine/Actions/HoseFlow.swift index 6482139..62c2b6a 100644 --- a/Sources/StitchEngine/Actions/HoseFlow.swift +++ b/Sources/StitchEngine/Actions/HoseFlow.swift @@ -68,41 +68,35 @@ extension GraphCalculatable { var changedIds = Set() for inputCoordinate in inputs { - // Catch cycle red edge case - if self.topologicalData.cycleStartingPoints.contains(inputCoordinate.nodeId) { - self.topologicalData.cycleNodesForNextGraphStep.insert( - .init(portId: inputCoordinate, - values: upstreamOutputValues, - mediaList: mediaList) - ) - - continue - } + let didChangeInputs = self.updateInputs(inputCoordinate: inputCoordinate, + upstreamOutputValues: upstreamOutputValues, + mediaList: mediaList, + upstreamOutputChanged: upstreamOutputChanged, + cycleStartingPoints: self.topologicalData.cycleStartingPoints) - let newChangedIds = self.updateInputs(inputCoordinate: inputCoordinate, - upstreamOutputValues: upstreamOutputValues, - mediaList: mediaList, - upstreamOutputChanged: upstreamOutputChanged) - - changedIds = changedIds.union(newChangedIds) + if didChangeInputs { + changedIds.insert(inputCoordinate) + } } // for inputCoordinate in ... return changedIds } @MainActor + /// Returns: true if inputs had changed. func updateInputs(inputCoordinate: Self.Node.InputRow.RowID, - upstreamOutputValues: [Self.Node.PortData], - mediaList: [Self.Node.EvalResult.MediaType?]?, - upstreamOutputChanged: Bool) -> Set { + upstreamOutputValues: [Self.Node.PortData], + mediaList: [Self.Node.EvalResult.MediaType?]?, + upstreamOutputChanged: Bool, + cycleStartingPoints: Set) -> Bool { // Get kind of downstream node guard let nodeViewModel = self.getNode(id: inputCoordinate.nodeId), let inputObserver = nodeViewModel.getInputRowObserver(for: inputCoordinate.portType) else { return .init() } - var changedIds = Set() let inputOldValues = inputObserver.values + var didChange = false if inputObserver.isPulseNodeType && !upstreamOutputChanged { // If this is a pulse type input and the upstream output did not change, @@ -126,9 +120,26 @@ extension GraphCalculatable { currentGraphTime: self.currentGraphTime) if inputOldValues != flowValuesCoercedToThisInputType { + // Catch cycle red edge case if already tracked +// let isRedEdgeAlreadyTracked = self.topologicalData.cycleNodesForNextGraphStep.contains(where: { +// $0.nodeId == inputCoordinate.nodeId +// }) + + // Catch cycle red edge case if not yet tracked + if cycleStartingPoints.contains(inputCoordinate.nodeId) { + self.topologicalData.cycleNodesForNextGraphStep.insert( + .init(portId: inputCoordinate, + values: upstreamOutputValues, + mediaList: mediaList) + ) + + // Skip updating this node on this cycle + return false + } + inputObserver.setValuesInInput(flowValuesCoercedToThisInputType) - changedIds.insert(inputObserver.id) + didChange = true // Update downstream observers if let mediaList = mediaList, @@ -139,6 +150,6 @@ extension GraphCalculatable { } } - return changedIds + return didChange } } From 97af57f4811f9036f5410fd05ed04738ea859ade Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Fri, 18 Apr 2025 16:37:22 -0700 Subject: [PATCH 4/6] removes redundant cycle nodes --- .../Actions/TopologicalDataRefresh.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift b/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift index 602a19f..341af2e 100644 --- a/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift +++ b/Sources/StitchEngine/Actions/TopologicalDataRefresh.swift @@ -36,8 +36,9 @@ extension GraphCalculatable { self.nodesForNextGraphStep = prevIdsToCalculate // Gets a set of all node cycles - self.topologicalData.nodeCycles = TopologicalData + let nodeCycles = TopologicalData .findAllCycles(downstreamNodesMap: downstream) + self.topologicalData.nodeCycles = nodeCycles self.topologicalData.shallowDownstreamNodes = downstream @@ -56,7 +57,17 @@ extension GraphCalculatable { redEdgeCycles: .init()) { queuedNodeResult, queue in switch queuedNodeResult { case .cycle(let nextCycleNodeResult): - cycleStartingPoints.insert(nextCycleNodeResult.nodeId) + guard let allNodesInCycle = nodeCycles.first(where: { + $0.contains(nextCycleNodeResult.nodeId) + }) else { + fatalErrorIfDebug() + return + } + + // Only add red edge if this cycle not yet tracked + if cycleStartingPoints.intersection(allNodesInCycle).isEmpty { + cycleStartingPoints.insert(nextCycleNodeResult.nodeId) + } default: return From 3591d3e07f29f48a40f0f929572fb8aea079e669 Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Fri, 18 Apr 2025 20:17:16 -0700 Subject: [PATCH 5/6] fixed values changing --- Sources/StitchEngine/Actions/HoseFlow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StitchEngine/Actions/HoseFlow.swift b/Sources/StitchEngine/Actions/HoseFlow.swift index 62c2b6a..475f3c2 100644 --- a/Sources/StitchEngine/Actions/HoseFlow.swift +++ b/Sources/StitchEngine/Actions/HoseFlow.swift @@ -129,7 +129,7 @@ extension GraphCalculatable { if cycleStartingPoints.contains(inputCoordinate.nodeId) { self.topologicalData.cycleNodesForNextGraphStep.insert( .init(portId: inputCoordinate, - values: upstreamOutputValues, + values: flowValuesCoercedToThisInputType, mediaList: mediaList) ) From dd9d525b4d1eb6f59223f5369b45472e6a249090 Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Fri, 18 Apr 2025 21:59:02 -0700 Subject: [PATCH 6/6] pulse fix --- Sources/StitchEngine/Actions/HoseFlow.swift | 11 ++++------- .../StitchEngine/Data/Node/NodeRowCalculatable.swift | 3 ++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Sources/StitchEngine/Actions/HoseFlow.swift b/Sources/StitchEngine/Actions/HoseFlow.swift index 475f3c2..4df5181 100644 --- a/Sources/StitchEngine/Actions/HoseFlow.swift +++ b/Sources/StitchEngine/Actions/HoseFlow.swift @@ -105,6 +105,7 @@ extension GraphCalculatable { return .init() // } else if upstreamOutputChanged { } else { + let isCycleRedEdge = cycleStartingPoints.contains(inputCoordinate.nodeId) guard let existingInputValue = inputOldValues.first else { return .init() @@ -117,16 +118,12 @@ extension GraphCalculatable { let flowValuesCoercedToThisInputType = inputObserver.coerce( theseValues: upstreamOutputValues, toThisType: existingInputValue, - currentGraphTime: self.currentGraphTime) + currentGraphTime: self.currentGraphTime, + isCycleRedEdge: isCycleRedEdge) if inputOldValues != flowValuesCoercedToThisInputType { - // Catch cycle red edge case if already tracked -// let isRedEdgeAlreadyTracked = self.topologicalData.cycleNodesForNextGraphStep.contains(where: { -// $0.nodeId == inputCoordinate.nodeId -// }) - // Catch cycle red edge case if not yet tracked - if cycleStartingPoints.contains(inputCoordinate.nodeId) { + if isCycleRedEdge { self.topologicalData.cycleNodesForNextGraphStep.insert( .init(portId: inputCoordinate, values: flowValuesCoercedToThisInputType, diff --git a/Sources/StitchEngine/Data/Node/NodeRowCalculatable.swift b/Sources/StitchEngine/Data/Node/NodeRowCalculatable.swift index 9f96237..7866be4 100644 --- a/Sources/StitchEngine/Data/Node/NodeRowCalculatable.swift +++ b/Sources/StitchEngine/Data/Node/NodeRowCalculatable.swift @@ -32,7 +32,8 @@ public protocol InputNodeRowCalculatable: NodeRowCalculatable { @MainActor func coerce(theseValues: [PortData], toThisType: PortData, - currentGraphTime: TimeInterval) -> [PortData] + currentGraphTime: TimeInterval, + isCycleRedEdge: Bool) -> [PortData] @MainActor func didInputsUpdate(newValues: [PortData], oldValues: [PortData])