Skip to content

Commit 91ebc16

Browse files
authored
Merge pull request #2504 from alicevision/dev/forLoop
First version of For Loop implementation
2 parents c4cc5b6 + 85bfc56 commit 91ebc16

File tree

11 files changed

+392
-126
lines changed

11 files changed

+392
-126
lines changed

meshroom/core/attribute.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def attributeFactory(description, value, isOutput, node, root=None, parent=None)
3636
class Attribute(BaseObject):
3737
"""
3838
"""
39-
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.]*\}$')
39+
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.\[\]]*\}$')
4040

4141
def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
4242
"""
@@ -324,6 +324,9 @@ def hasOutputConnections(self):
324324
# safety check to avoid evaluation errors
325325
if not self.node.graph or not self.node.graph.edges:
326326
return False
327+
# if the attribute is a ListAttribute, we need to check if any of its elements has output connections
328+
if isinstance(self, ListAttribute):
329+
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None or any(attr.hasOutputConnections for attr in self._value if hasattr(attr, 'hasOutputConnections'))
327330
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None
328331

329332
def _applyExpr(self):
@@ -447,6 +450,7 @@ def updateInternals(self):
447450
uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True)
448451
validValueChanged = Signal()
449452
validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged)
453+
root = Property(BaseObject, root.fget, constant=True)
450454

451455

452456
def raiseIfLink(func):

meshroom/ui/graph.py

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,8 @@ def removeNodesFrom(self, nodes):
687687
Args:
688688
startNode (Node): the node to start from.
689689
"""
690+
if isinstance(nodes, Node):
691+
nodes = [nodes]
690692
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
691693
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
692694
# filter out nodes that will be removed more than once
@@ -706,7 +708,7 @@ def duplicateNodes(self, nodes):
706708
list[Node]: the list of duplicated nodes
707709
"""
708710
nodes = self.filterNodes(nodes)
709-
nPositions = []
711+
nPositions = [(n.x, n.y) for n in self._graph.nodes]
710712
# enable updates between duplication and layout to get correct depths during layout
711713
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
712714
# disable graph updates during duplication
@@ -716,9 +718,8 @@ def duplicateNodes(self, nodes):
716718
bbox = self._layout.boundingBox(nodes)
717719

718720
for n in duplicates:
719-
idx = duplicates.index(n)
720721
yPos = n.y + self.layout.gridSpacing + bbox[3]
721-
if idx > 0 and (n.x, yPos) in nPositions:
722+
if (n.x, yPos) in nPositions:
722723
# make sure the node will not be moved on top of another node
723724
while (n.x, yPos) in nPositions:
724725
yPos = yPos + self.layout.gridSpacing + self.layout.nodeHeight
@@ -739,12 +740,62 @@ def duplicateNodesFrom(self, nodes):
739740
Returns:
740741
list[Node]: the list of duplicated nodes
741742
"""
743+
if isinstance(nodes, Node):
744+
nodes = [nodes]
742745
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
743746
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
744747
# filter out nodes that will be duplicated more than once
745748
uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate))
746749
duplicates = self.duplicateNodes(uniqueNodesToDuplicate)
747750
return duplicates
751+
752+
@Slot(Edge, result=bool)
753+
def canExpandForLoop(self, currentEdge):
754+
""" Check if the list attribute can be expanded by looking at all the edges connected to it. """
755+
listAttribute = currentEdge.src.root
756+
if not listAttribute:
757+
return False
758+
srcIndex = listAttribute.index(currentEdge.src)
759+
allSrc = [e.src for e in self._graph.edges.values()]
760+
for i in range(len(listAttribute)):
761+
if i == srcIndex:
762+
continue
763+
if listAttribute.at(i) in allSrc:
764+
return False
765+
return True
766+
767+
@Slot(Edge, result=Edge)
768+
def expandForLoop(self, currentEdge):
769+
""" Expand 'node' by creating all its output nodes. """
770+
with self.groupedGraphModification("Expand For Loop Node"):
771+
listAttribute = currentEdge.src.root
772+
dst = currentEdge.dst
773+
774+
for i in range(1, len(listAttribute)):
775+
duplicates = self.duplicateNodesFrom(dst.node)
776+
newNode = duplicates[0]
777+
previousEdge = self.graph.edge(newNode.attribute(dst.name))
778+
self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst)
779+
780+
# Last, replace the edge with the first element of the list
781+
return self.replaceEdge(currentEdge, listAttribute.at(0), dst)
782+
783+
@Slot(Edge)
784+
def collapseForLoop(self, currentEdge):
785+
""" Collapse 'node' by removing all its output nodes. """
786+
with self.groupedGraphModification("Collapse For Loop Node"):
787+
listAttribute = currentEdge.src.root
788+
srcIndex = listAttribute.index(currentEdge.src)
789+
allSrc = [e.src for e in self._graph.edges.values()]
790+
for i in reversed(range(len(listAttribute))):
791+
if i == srcIndex:
792+
continue
793+
occurence = allSrc.index(listAttribute.at(i)) if listAttribute.at(i) in allSrc else -1
794+
if occurence != -1:
795+
self.removeNodesFrom(self.graph.edges.at(occurence).dst.node)
796+
# update the edges from allSrc
797+
allSrc = [e.src for e in self._graph.edges.values()]
798+
748799

749800
@Slot(QObject)
750801
def clearData(self, nodes):
@@ -765,7 +816,9 @@ def clearDataFrom(self, nodes):
765816

766817
@Slot(Attribute, Attribute)
767818
def addEdge(self, src, dst):
768-
if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute):
819+
if isinstance(src, ListAttribute) and not isinstance(dst, ListAttribute):
820+
self._addEdge(src.at(0), dst)
821+
elif isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute):
769822
with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullNameToNode())):
770823
self.appendAttribute(dst)
771824
self._addEdge(src, dst.at(-1))
@@ -787,14 +840,32 @@ def removeEdge(self, edge):
787840
else:
788841
self.push(commands.RemoveEdgeCommand(self._graph, edge))
789842

843+
@Slot(Edge, Attribute, Attribute, result=Edge)
844+
def replaceEdge(self, edge, newSrc, newDst):
845+
with self.groupedGraphModification("Replace Edge '{}'->'{}' with '{}'->'{}'".format(edge.src.getFullNameToNode(), edge.dst.getFullNameToNode(), newSrc.getFullNameToNode(), newDst.getFullNameToNode())):
846+
self.removeEdge(edge)
847+
self.addEdge(newSrc, newDst)
848+
return self._graph.edge(newDst)
849+
850+
@Slot(Attribute, result=Edge)
851+
def getEdge(self, dst):
852+
return self._graph.edge(dst)
853+
790854
@Slot(Attribute, "QVariant")
791855
def setAttribute(self, attribute, value):
792856
self.push(commands.SetAttributeCommand(self._graph, attribute, value))
793857

794858
@Slot(Attribute)
795859
def resetAttribute(self, attribute):
796860
""" Reset 'attribute' to its default value """
797-
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
861+
with self.groupedGraphModification("Reset Attribute '{}'".format(attribute.name)):
862+
# if the attribute is a ListAttribute, remove all edges
863+
if isinstance(attribute, ListAttribute):
864+
for edge in self._graph.edges:
865+
# if the edge is connected to one of the ListAttribute's elements, remove it
866+
if edge.src in attribute.value:
867+
self.removeEdge(edge)
868+
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
798869

799870
@Slot(CompatibilityNode, result=Node)
800871
def upgradeNode(self, node):
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import QtQuick 2.15
2+
import MaterialIcons 2.2
3+
import QtQuick.Controls 2.15
4+
import QtQuick.Layouts 1.11
5+
6+
/*
7+
* IntSelector with arrows and a text input to select a number
8+
*/
9+
10+
Row {
11+
id: root
12+
13+
property string tooltipText: ""
14+
property int value: 0
15+
property var range: { "min" : 0, "max" : 0 }
16+
17+
Layout.alignment: Qt.AlignVCenter
18+
19+
spacing: 0
20+
property bool displayButtons: previousIntButton.hovered || intInputMouseArea.containsMouse || nextIntButton.hovered
21+
property real buttonsOpacity: displayButtons ? 1.0 : 0.0
22+
23+
MaterialToolButton {
24+
id: previousIntButton
25+
26+
opacity: buttonsOpacity
27+
width: 10
28+
text: MaterialIcons.navigate_before
29+
ToolTip.text: "Previous"
30+
31+
onClicked: {
32+
if (value > range.min) {
33+
value -= 1
34+
}
35+
}
36+
}
37+
38+
TextInput {
39+
id: intInput
40+
41+
ToolTip.text: tooltipText
42+
ToolTip.visible: tooltipText && intInputMouseArea.containsMouse
43+
44+
width: intMetrics.width
45+
height: previousIntButton.height
46+
47+
color: palette.text
48+
horizontalAlignment: Text.AlignHCenter
49+
verticalAlignment: Text.AlignVCenter
50+
selectByMouse: true
51+
52+
text: value
53+
54+
onEditingFinished: {
55+
// We first assign the frame to the entered text even if it is an invalid frame number. We do it for extreme cases, for example without doing it, if we are at 0, and put a negative number, value would be still 0 and nothing happens but we will still see the wrong number
56+
value = parseInt(text)
57+
value = Math.min(range.max, Math.max(range.min, parseInt(text)))
58+
focus = false
59+
}
60+
61+
MouseArea {
62+
id: intInputMouseArea
63+
anchors.fill: parent
64+
hoverEnabled: true
65+
acceptedButtons: Qt.NoButton
66+
propagateComposedEvents: true
67+
}
68+
}
69+
70+
MaterialToolButton {
71+
id: nextIntButton
72+
73+
width: 10
74+
opacity: buttonsOpacity
75+
text: MaterialIcons.navigate_next
76+
ToolTip.text: "Next"
77+
78+
onClicked: {
79+
if (value < range.max) {
80+
value += 1
81+
}
82+
}
83+
}
84+
85+
TextMetrics {
86+
id: intMetrics
87+
88+
font: intInput.font
89+
text: "10000"
90+
}
91+
92+
}

meshroom/ui/qml/Controls/qmldir

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ TabPanel 1.0 TabPanel.qml
1212
TextFileViewer 1.0 TextFileViewer.qml
1313
ExifOrientedViewer 1.0 ExifOrientedViewer.qml
1414
FilterComboBox 1.0 FilterComboBox.qml
15+
IntSelector 1.0 IntSelector.qml

meshroom/ui/qml/GraphEditor/AttributePin.qml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ RowLayout {
100100
|| drag.source.objectName != inputDragTarget.objectName // not an edge connector
101101
|| drag.source.baseType !== inputDragTarget.baseType // not the same base type
102102
|| drag.source.nodeItem === inputDragTarget.nodeItem // connection between attributes of the same node
103-
|| (drag.source.isList && !inputDragTarget.isList) // connection between a list and a simple attribute
104103
|| (drag.source.isList && childrenRepeater.count) // source/target are lists but target already has children
105104
|| drag.source.connectorType === "input" // refuse to connect an "input pin" on another one (input attr can be connected to input attr, but not the graphical pin)
106105
) {

0 commit comments

Comments
 (0)