diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 79d70f1f72..9ed4d62b4f 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -58,6 +58,65 @@ 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 + forLoopAttribute = None + if currentNode is not None: + for attr in currentNode._attributes: + if attr.isInput and attr.isLink: + forLoopAttribute = currentNode._attributes.indexOf(attr) + 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)): + # name of the iteration node + name = "{}_{}".format(currentNode.name, i) + # check if node already exists + if name not in [n.name for n in self._parentNode.graph.nodes]: + iterationNode = IterationNode(currentNode, i, forLoopAttribute) + self._parentNode.graph.addNode(iterationNode, iterationNode.name) + else : + # find node by name + iterationNode = self._parentNode.graph.node(name) + iterationNode._updateChunks() + + self._iterations.append(iterationNode) + + print("parent internal folder: ", currentNode.internalFolder) + 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 +572,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) @@ -979,6 +1039,10 @@ def updateInternals(self, cacheDir=None): } self._computeUids() self._buildCmdVars() + + # try to update for loopdata as node is created + self._forLoopData.update(self) + if self.nodeDesc: self.nodeDesc.postUpdate(self) # Notify internal folder change if needed @@ -1320,6 +1384,13 @@ def has3DOutputAttribute(self): if attr.enabled and attr.isOutput and hasSupportedExt: return True return False + + def getForLoopData(self): + """ + Return the ForLoopData of the node. + """ + return self._forLoopData + name = Property(str, getName, constant=True) @@ -1373,6 +1444,9 @@ def has3DOutputAttribute(self): hasSequenceOutput = Property(bool, hasSequenceOutputAttribute, notify=outputAttrEnabledChanged) has3DOutput = Property(bool, has3DOutputAttribute, notify=outputAttrEnabledChanged) + forLoopDataChanged = Signal() + forLoopData = Property(Variant, getForLoopData, notify=forLoopDataChanged) + class Node(BaseNode): """ A standard Graph node based on a node type. @@ -1530,7 +1604,39 @@ def _updateChunks(self): else: self._chunks[0].range = desc.Range() +class IterationNode(Node): + """ + 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, attributeIndex): + super(IterationNode, self).__init__(node.nodeType, parent=node.graph) + self._name = f"{node.name}_{iteration}" + self._originalNode = node + + # By recognizing the connected attribute linked to the ForLoop node, we can set the value of the iteration + # attribute to the current iteration value + self._attributes.at(attributeIndex).value = node._forLoopData.connectedAttribute.at(iteration).value + + print("Attributes of IterationNode: ", [attr.value for attr in self._attributes.values()]) + + # Internal folder should correspond to each possibility of uid + # print(self._uids) + # self._buildCmdVars() + # self._computeUids() + # print("after: ", self._uids) + print(self.nodeType) + print(self._cmdVars) + print(self._internalFolder) + def _updateChunks(self): + # find node in graph + node = self.graph.node(self._originalNode.name) + # Setting chunks to the same chunks as the parent node + self._chunks.setObjectList([NodeChunk(self, desc.Range()) for _ in node._chunks]) + for c in self._chunks: + c.statusChanged.connect(self.globalStatusChanged) + + self.chunksChanged.emit() class CompatibilityIssue(Enum): """ Enum describing compatibility issues when deserializing a Node. diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 448f25484a..c248d09349 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 @@ -634,7 +634,10 @@ def addNewNode(self, nodeType, position=None, **kwargs): """ if isinstance(position, QPoint): position = Position(position.x(), position.y()) - return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs)) + + node = self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs)) + self.nodesChanged.emit() + return node def filterNodes(self, nodes): """Filter out the nodes that do not exist on the graph.""" @@ -818,6 +821,7 @@ def clearDataFrom(self, nodes): def addEdge(self, src, dst): if isinstance(src, ListAttribute) and not isinstance(dst, ListAttribute): self._addEdge(src.at(0), dst) + self.nodesChanged.emit() elif isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute): with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullNameToNode())): self.appendAttribute(dst) @@ -1119,12 +1123,27 @@ 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) + nodesChanged = Signal() + nodes = Property(QObject, getNodes, notify=nodesChanged) 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 47ecdfafb7..d9efe12bb8 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -276,6 +276,16 @@ Item { } } + // Is in for loop indicator + MaterialLabel { + visible: node.forLoopData.countForLoop > 0 + text: MaterialIcons.loop + padding: 2 + font.pointSize: 7 + palette.text: Colors.sysPalette.text + ToolTip.text: "Is in " + node.forLoopData.countForLoop + " for loop(s)" + } + // Submitted externally indicator MaterialLabel { visible: ["SUBMITTED", "RUNNING"].includes(node.globalStatus) && node.chunks.count > 0 && node.isExternal @@ -367,19 +377,44 @@ Item { } // Node Chunks - NodeChunks { - visible: node.isComputable - defaultColor: Colors.sysPalette.mid - implicitHeight: 3 - width: parent.width - model: node ? node.chunks : undefined - - Rectangle { - anchors.fill: parent - color: Colors.sysPalette.mid - z: -1 - } - } + Column { + width: parent.width + + spacing: 2 + Repeater { + // the model is the number of iterations for the for loop + // so if the count is 0 we display only one iteration + // else we display the number of iterations + model: { + 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 + } + } + + delegate: NodeChunks { + visible: node.isComputable + defaultColor: Colors.sysPalette.mid + height: 3 + width: parent.width + model: { + return modelData.chunks + } + + Rectangle { + anchors.fill: parent + color: Colors.sysPalette.mid + z: -1 + } + } + } + } // Vertical Spacer Item { width: parent.width; height: 2 } diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 3d4684229e..aa5c583827 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -257,11 +257,48 @@ Panel { Controls1.SplitView { anchors.fill: parent + // The list of iterations + + 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 + } + + 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 - ChunksListView { + NodeEditorElementsListView { id: chunksLV visible: (tabBar.currentIndex >= 1 && tabBar.currentIndex <= 3) - chunks: root.node.chunks + elements: root.node.chunks } StackLayout { @@ -295,7 +332,7 @@ Panel { id: nodeLog node: root.node currentChunkIndex: chunksLV.currentIndex - currentChunk: chunksLV.currentChunk + currentChunk: chunksLV.currentElement } } @@ -310,7 +347,7 @@ Panel { Layout.fillWidth: true node: root.node currentChunkIndex: chunksLV.currentIndex - currentChunk: chunksLV.currentChunk + currentChunk: chunksLV.currentElement } } @@ -325,7 +362,7 @@ Panel { Layout.fillWidth: true node: root.node currentChunkIndex: chunksLV.currentIndex - currentChunk: chunksLV.currentChunk + currentChunk: chunksLV.currentElement } } diff --git a/meshroom/ui/qml/GraphEditor/ChunksListView.qml b/meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml similarity index 55% rename from meshroom/ui/qml/GraphEditor/ChunksListView.qml rename to meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml index c8fd39cba8..9159dfc240 100644 --- a/meshroom/ui/qml/GraphEditor/ChunksListView.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditorElementsListView.qml @@ -8,52 +8,56 @@ import Controls 1.0 import "common.js" as Common /** - * ChunkListView + * NodeEditorElementsListView */ ColumnLayout { id: root - property variant chunks + property variant elements property int currentIndex: 0 - property variant currentChunk: (chunks && currentIndex >= 0) ? chunks.at(currentIndex) : undefined + property bool isChunk: true + property string title: "Chunks" - onChunksChanged: { + // TODO : change to currentElement + property variant currentElement: (elements && currentIndex >= 0) ? elements.at(currentIndex) : undefined + + onElementsChanged: { // When the list changes, ensure the current index is in the new range - if (currentIndex >= chunks.count) - currentIndex = chunks.count-1 + if (currentIndex >= elements.count) + currentIndex = elements.count-1 } - // chunksSummary is in sync with allChunks button (but not directly accessible as it is in a Component) - property bool chunksSummary: (currentIndex === -1) + // elementsSummary is in sync with allElements button (but not directly accessible as it is in a Component) + property bool elementsSummary: (currentIndex === -1) - width: 60 + width: 75 ListView { - id: chunksLV + id: elementsLV Layout.fillWidth: true Layout.fillHeight: true - model: root.chunks + model: root.elements - highlightFollowsCurrentItem: (root.chunksSummary === false) + highlightFollowsCurrentItem: (root.elementsSummary === false) keyNavigationEnabled: true focus: true currentIndex: root.currentIndex onCurrentIndexChanged: { - if (chunksLV.currentIndex !== root.currentIndex) { + if (elementsLV.currentIndex !== root.currentIndex) { // When the list is resized, the currentIndex is reset to 0. // So here we force it to keep the binding. - chunksLV.currentIndex = Qt.binding(function() { return root.currentIndex }) + elementsLV.currentIndex = Qt.binding(function() { return root.currentIndex }) } } header: Component { Button { - id: allChunks - text: "Chunks" + id: allElements + text: title width: parent.width flat: true checkable: true - property bool summaryEnabled: root.chunksSummary + property bool summaryEnabled: root.elementsSummary checked: summaryEnabled onSummaryEnabledChanged: { checked = summaryEnabled @@ -66,7 +70,7 @@ ColumnLayout { } highlight: Component { Rectangle { - visible: true // !root.chunksSummary + visible: true // !root.elementsSummary color: activePalette.highlight opacity: 0.3 z: 2 @@ -76,19 +80,25 @@ ColumnLayout { highlightResizeDuration: 0 delegate: ItemDelegate { - id: chunkDelegate - property var chunk: object + id: elementDelegate + property var element: object text: index width: parent ? parent.width : 0 leftPadding: 8 onClicked: { - chunksLV.forceActiveFocus() + elementsLV.forceActiveFocus() root.currentIndex = index } Rectangle { width: 4 height: parent.height - color: Common.getChunkColor(parent.chunk) + color: { + if (isChunk) { + return Common.getChunkColor(parent.element) + } else { + return Common.getNodeColor(parent.element) + } + } } } } diff --git a/meshroom/ui/qml/GraphEditor/common.js b/meshroom/ui/qml/GraphEditor/common.js index 35c754ce29..60a29d88c0 100644 --- a/meshroom/ui/qml/GraphEditor/common.js +++ b/meshroom/ui/qml/GraphEditor/common.js @@ -25,3 +25,7 @@ function getChunkColor(chunk, overrides) { console.warn("Unknown status : " + chunk.status) return "magenta" } + +function getNodeColor(node) { + return statusColors[node.globalStatus] || "magenta" +}