diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 0eb7bf4324..cad9e4d8a3 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -899,7 +899,6 @@ def addEdge(self, srcAttr, dstAttr): dstAttr.valueChanged.emit() dstAttr.isLinkChanged.emit() srcAttr.hasOutputConnectionsChanged.emit() - dstAttr.node.countForLoopChanged.emit() return edge def addEdges(self, *edges): @@ -916,7 +915,6 @@ def removeEdge(self, dstAttr): dstAttr.valueChanged.emit() dstAttr.isLinkChanged.emit() edge.src.hasOutputConnectionsChanged.emit() - dstAttr.node.countForLoopChanged.emit() def getDepth(self, node, minimal=False): """ Return node's depth in this Graph. @@ -1487,7 +1485,6 @@ def markNodesDirty(self, fromNode): nodes, edges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True) for node in nodes: node.dirty = True - node.countForLoopChanged.emit() def stopExecution(self): """ Request graph execution to be stopped by terminating running chunks""" diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 89cf763abe..e0b68adc6f 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -58,6 +58,55 @@ class ExecMode(Enum): LOCAL = 1 EXTERN = 2 +class ForLoopData(BaseObject): + """ + """ + + def __init__(self, parentNode=None, connectedAttribute=None, parent=None): + super(ForLoopData, self).__init__(parent) + self._countForLoop = 0 + self._iterations = ListModel(parent=self) # list of nodes for each iteration + self._parentNode = parentNode # parent node + self.connectedAttribute = connectedAttribute # attribute connected to the ForLoop node from parent node + + def update(self, currentNode=None): + # set the connectedAttribute + if currentNode is not None: + for attr in currentNode._attributes: + if attr.isInput and attr.isLink: + srcAttr = attr.getLinkParam() + # If the srcAttr is a ListAttribute, it means that the node is in a ForLoop + if isinstance(srcAttr.root, ListAttribute) and srcAttr.type == attr.type: + self.connectedAttribute = srcAttr.root + self._parentNode = srcAttr.root.node + break + + # set the countForLoop + if self.connectedAttribute is not None: + self._countForLoop = self._parentNode._forLoopData._countForLoop + 1 + if self.connectedAttribute.isInput: + self._countForLoop -= 1 if self._countForLoop > 1 else 1 + + # set the iterations by creating iteration nodes for each connected attribute value and will add them to the core graph and not the ui one + for i in range(len(self.connectedAttribute.value)): + iterationNode = IterationNode(currentNode, i) + # check if node already exists + if iterationNode.name not in [n.name for n in self._parentNode.graph.nodes]: + self._parentNode.graph.addNode(iterationNode, iterationNode.name) + self._iterations.append(iterationNode) + + self.parentNodeChanged.emit() + self.iterationsChanged.emit() + self.countForLoopChanged.emit() + + countForLoopChanged = Signal() + countForLoop = Property(int, lambda self: self._countForLoop, notify=countForLoopChanged) + iterationsChanged = Signal() + iterations = Property(Variant, lambda self: self._iterations, notify=iterationsChanged) + parentNodeChanged = Signal() + parentNode = Property(Variant, lambda self: self._parentNode, notify=parentNodeChanged) + + class StatusData(BaseObject): """ @@ -513,6 +562,7 @@ def __init__(self, nodeType, position=None, parent=None, uids=None, **kwargs): self._locked = False self._duplicates = ListModel(parent=self) # list of nodes with the same uid self._hasDuplicates = False + self._forLoopData = ForLoopData() self.globalStatusChanged.connect(self.updateDuplicatesStatusAndLocked) @@ -967,6 +1017,10 @@ def updateInternals(self, cacheDir=None): # Update chunks splitting self._updateChunks() + + # try to update for loopdata as node is created + self._forLoopData.update(self) + # Retrieve current internal folder (if possible) try: folder = self.internalFolder @@ -1321,23 +1375,11 @@ def has3DOutputAttribute(self): return True return False - def _countForLoop(self): + def getForLoopData(self): """ - Return in how many ForLoop nodes this node is. + Return the ForLoopData of the node. """ - count = 0 - # Access to the input attributes of the node - for attr in self._attributes: - if attr.isInput and attr.isLink: - # Access to the attribute connected to the input attribute - srcAttr = attr.getLinkParam() - # If the srcAttr is a ListAttribute, it means that the node is in a ForLoop - if isinstance(srcAttr.root, ListAttribute) and srcAttr.type == attr.type: - # Access the countForLoop of the node of the ListAttribute - count = srcAttr.root.node.countForLoop + 1 - if srcAttr.root.isInput: - count = count - 1 if count > 1 else 1 - return count + return self._forLoopData @@ -1392,8 +1434,8 @@ def _countForLoop(self): hasSequenceOutput = Property(bool, hasSequenceOutputAttribute, notify=outputAttrEnabledChanged) has3DOutput = Property(bool, has3DOutputAttribute, notify=outputAttrEnabledChanged) - countForLoopChanged = Signal() - countForLoop = Property(int, _countForLoop, notify=countForLoopChanged) + forLoopDataChanged = Signal() + forLoopData = Property(Variant, getForLoopData, notify=forLoopDataChanged) class Node(BaseNode): """ @@ -1553,6 +1595,22 @@ def _updateChunks(self): self._chunks[0].range = desc.Range() +class IterationNode(BaseNode): + """ + A node that is not added to the graph but used to process a specific iteration of a ForLoop node. + """ + def __init__(self, node, iteration): + super(IterationNode, self).__init__(node.nodeType, parent=node.graph, uids=node._uids) + self._name = f"{node.name}_{iteration}" + + self._chunks.setObjectList([NodeChunk(self, desc.Range(iteration, iteration))]) + + # if iteration is 3 set status to success + if iteration == 3 or iteration == 12: + self._chunks.at(0).status.status = Status.SUCCESS + + # print(self._attributes.at(0)) + class CompatibilityIssue(Enum): """ Enum describing compatibility issues when deserializing a Node. diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index caac4e8e60..c3144ba6dc 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -17,7 +17,7 @@ from meshroom.core.taskManager import TaskManager -from meshroom.core.node import NodeChunk, Node, Status, ExecMode, CompatibilityNode, Position +from meshroom.core.node import NodeChunk, Node, Status, ExecMode, CompatibilityNode, Position, IterationNode from meshroom.core import submitters from meshroom.ui import commands from meshroom.ui.utils import makeProperty @@ -776,7 +776,6 @@ def expandForLoop(self, currentEdge): newNode = duplicates[0] previousEdge = self.graph.edge(newNode.attribute(dst.name)) self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst) - newNode.countForLoopChanged.emit() # Last, replace the edge with the first element of the list return self.replaceEdge(currentEdge, listAttribute.at(0), dst) @@ -1120,12 +1119,26 @@ def pasteNodes(self, clipboardContent, position=None, centerPosition=False): positions.append(finalPosition) return self.push(commands.PasteNodesCommand(self.graph, d, position=positions)) + + def getNodes(self): + """ + Return all the nodes that are not Iteration nodes. + """ + nodes = self._graph.nodes + toRemove = [] + for node in nodes.values(): + if isinstance(node, IterationNode): + toRemove.append(node) + for node in toRemove: + nodes.pop(node.name) + return nodes + undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graphChanged = Signal() graph = Property(Graph, lambda self: self._graph, notify=graphChanged) taskManager = Property(TaskManager, lambda self: self._taskManager, constant=True) - nodes = Property(QObject, lambda self: self._graph.nodes, notify=graphChanged) + nodes = Property(QObject, getNodes, notify=graphChanged) layout = Property(GraphLayout, lambda self: self._layout, constant=True) computeStatusChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index eabfd393c9..6ae7856b81 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -1085,7 +1085,7 @@ Item { Repeater { id: filteredNodes model: SortFilterDelegateModel { - model: root.graph ? root.graph.nodes : undefined + model: root.uigraph ? root.uigraph.nodes : undefined sortRole: "label" filters: [{role: "label", value: graphSearchBar.text}] delegate: Item { diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 4ddca08972..d9efe12bb8 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -278,12 +278,12 @@ Item { // Is in for loop indicator MaterialLabel { - visible: node.countForLoop > 0 + visible: node.forLoopData.countForLoop > 0 text: MaterialIcons.loop padding: 2 font.pointSize: 7 palette.text: Colors.sysPalette.text - ToolTip.text: "Is in " + node.countForLoop + " for loop(s)" + ToolTip.text: "Is in " + node.forLoopData.countForLoop + " for loop(s)" } // Submitted externally indicator @@ -386,14 +386,15 @@ Item { // so if the count is 0 we display only one iteration // else we display the number of iterations model: { - if (node.countForLoop === 0) - return 1 - - for (let i = 0; i < node.attributes.count; ++i) { - if (node.attributes.at(i).isLink) { - var srcAttr = node.attributes.at(i).linkParam - return srcAttr.root.value.count + if (node.forLoopData.countForLoop === 0) { + return node + } else { + // convert the iterations to a list + let list = [] + for (let i = 0; i < node.forLoopData.iterations.count; ++i) { + list.push(node.forLoopData.iterations.at(i)) } + return list } } @@ -402,7 +403,9 @@ Item { defaultColor: Colors.sysPalette.mid height: 3 width: parent.width - model: node ? node.chunks : undefined + model: { + return modelData.chunks + } Rectangle { anchors.fill: parent diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index fc570cb026..aa5c583827 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -258,29 +258,40 @@ Panel { anchors.fill: parent // The list of iterations - NodeEditorElementsListView { - id: iterationsLV - visible: root.node.countForLoop > 0 - elements: { - if (root.node.countForLoop == 0) - return [] - var elements = [] - for (let i = 0; i < node.attributes.count; ++i) { - if (node.attributes.at(i).isLink) { - var srcAttr = node.attributes.at(i).linkParam - for (let j = 0; j < srcAttr.root.value.count; ++j) { - elements.push(j) - } - return elements - } + + Repeater { + id: iterationsRepeater + visible: root.node.forLoopData.countForLoop > 0 + + model: { + let currentNode = root.node + let count = root.node.forLoopData.countForLoop + let list = [] + for (let i = 0; i < count; i++) { + let parent = currentNode.forLoopData.parentNode + list.push(currentNode.forLoopData.iterations) + currentNode = parent } + + // reverse the list + list.reverse() + return list } - // TODO to remove when the elements would be correct - currentElement: elements[0] - - isChunk: false - title: "Iterations" + NodeEditorElementsListView { + id: iterationsLV + elements: { + if (root.node.forLoopData.countForLoop == 0) + return [] + return modelData + } + + // TODO to remove when the elements would be correct + // currentElement: elements[0] + + isChunk: false + title: "Iterations" + } } // The list of chunks diff --git a/meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml b/meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml index 27039dc58a..4e44ea9dc2 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml @@ -92,7 +92,13 @@ ColumnLayout { Rectangle { width: 4 height: parent.height - color: isChunk ? Common.getChunkColor(parent.element) : palette.mid + color: { + if (isChunk) { + return Common.getChunkColor(parent.element) + } else { + return Common.getChunkColor(parent.element.chunks.at(0)) + } + } } } }