Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First version of For Loop implementation #2504

Merged
merged 17 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def attributeFactory(description, value, isOutput, node, root=None, parent=None)
class Attribute(BaseObject):
"""
"""
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.]*\}$')
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.\[\]]*\}$')

def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
"""
Expand Down Expand Up @@ -324,6 +324,9 @@ def hasOutputConnections(self):
# safety check to avoid evaluation errors
if not self.node.graph or not self.node.graph.edges:
return False
# if the attribute is a ListAttribute, we need to check if any of its elements has output connections
if isinstance(self, ListAttribute):
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'))
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None

def _applyExpr(self):
Expand Down Expand Up @@ -447,6 +450,7 @@ def updateInternals(self):
uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True)
validValueChanged = Signal()
validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged)
root = Property(BaseObject, root.fget, constant=True)


def raiseIfLink(func):
Expand Down
81 changes: 76 additions & 5 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,8 @@ def removeNodesFrom(self, nodes):
Args:
startNode (Node): the node to start from.
"""
if isinstance(nodes, Node):
nodes = [nodes]
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
# filter out nodes that will be removed more than once
Expand All @@ -706,7 +708,7 @@ def duplicateNodes(self, nodes):
list[Node]: the list of duplicated nodes
"""
nodes = self.filterNodes(nodes)
nPositions = []
nPositions = [(n.x, n.y) for n in self._graph.nodes]
# enable updates between duplication and layout to get correct depths during layout
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
# disable graph updates during duplication
Expand All @@ -716,9 +718,8 @@ def duplicateNodes(self, nodes):
bbox = self._layout.boundingBox(nodes)

for n in duplicates:
idx = duplicates.index(n)
yPos = n.y + self.layout.gridSpacing + bbox[3]
if idx > 0 and (n.x, yPos) in nPositions:
if (n.x, yPos) in nPositions:
# make sure the node will not be moved on top of another node
while (n.x, yPos) in nPositions:
yPos = yPos + self.layout.gridSpacing + self.layout.nodeHeight
Expand All @@ -739,12 +740,62 @@ def duplicateNodesFrom(self, nodes):
Returns:
list[Node]: the list of duplicated nodes
"""
if isinstance(nodes, Node):
nodes = [nodes]
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
# filter out nodes that will be duplicated more than once
uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate))
duplicates = self.duplicateNodes(uniqueNodesToDuplicate)
return duplicates

@Slot(Edge, result=bool)
def canExpandForLoop(self, currentEdge):
""" Check if the list attribute can be expanded by looking at all the edges connected to it. """
listAttribute = currentEdge.src.root
if not listAttribute:
return False
srcIndex = listAttribute.index(currentEdge.src)
allSrc = [e.src for e in self._graph.edges.values()]
for i in range(len(listAttribute)):
if i == srcIndex:
continue
if listAttribute.at(i) in allSrc:
return False
return True

@Slot(Edge, result=Edge)
def expandForLoop(self, currentEdge):
""" Expand 'node' by creating all its output nodes. """
with self.groupedGraphModification("Expand For Loop Node"):
listAttribute = currentEdge.src.root
dst = currentEdge.dst

for i in range(1, len(listAttribute)):
duplicates = self.duplicateNodesFrom(dst.node)
newNode = duplicates[0]
previousEdge = self.graph.edge(newNode.attribute(dst.name))
self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst)

# Last, replace the edge with the first element of the list
return self.replaceEdge(currentEdge, listAttribute.at(0), dst)

@Slot(Edge)
def collapseForLoop(self, currentEdge):
""" Collapse 'node' by removing all its output nodes. """
with self.groupedGraphModification("Collapse For Loop Node"):
listAttribute = currentEdge.src.root
srcIndex = listAttribute.index(currentEdge.src)
allSrc = [e.src for e in self._graph.edges.values()]
for i in reversed(range(len(listAttribute))):
if i == srcIndex:
continue
occurence = allSrc.index(listAttribute.at(i)) if listAttribute.at(i) in allSrc else -1
if occurence != -1:
self.removeNodesFrom(self.graph.edges.at(occurence).dst.node)
# update the edges from allSrc
allSrc = [e.src for e in self._graph.edges.values()]


@Slot(QObject)
def clearData(self, nodes):
Expand All @@ -765,7 +816,9 @@ def clearDataFrom(self, nodes):

@Slot(Attribute, Attribute)
def addEdge(self, src, dst):
if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute):
if isinstance(src, ListAttribute) and not isinstance(dst, ListAttribute):
self._addEdge(src.at(0), dst)
elif isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute):
with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullNameToNode())):
self.appendAttribute(dst)
self._addEdge(src, dst.at(-1))
Expand All @@ -787,14 +840,32 @@ def removeEdge(self, edge):
else:
self.push(commands.RemoveEdgeCommand(self._graph, edge))

@Slot(Edge, Attribute, Attribute, result=Edge)
def replaceEdge(self, edge, newSrc, newDst):
with self.groupedGraphModification("Replace Edge '{}'->'{}' with '{}'->'{}'".format(edge.src.getFullNameToNode(), edge.dst.getFullNameToNode(), newSrc.getFullNameToNode(), newDst.getFullNameToNode())):
self.removeEdge(edge)
self.addEdge(newSrc, newDst)
return self._graph.edge(newDst)

@Slot(Attribute, result=Edge)
def getEdge(self, dst):
return self._graph.edge(dst)

@Slot(Attribute, "QVariant")
def setAttribute(self, attribute, value):
self.push(commands.SetAttributeCommand(self._graph, attribute, value))

@Slot(Attribute)
def resetAttribute(self, attribute):
""" Reset 'attribute' to its default value """
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
with self.groupedGraphModification("Reset Attribute '{}'".format(attribute.name)):
# if the attribute is a ListAttribute, remove all edges
if isinstance(attribute, ListAttribute):
for edge in self._graph.edges:
# if the edge is connected to one of the ListAttribute's elements, remove it
if edge.src in attribute.value:
self.removeEdge(edge)
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))

@Slot(CompatibilityNode, result=Node)
def upgradeNode(self, node):
Expand Down
92 changes: 92 additions & 0 deletions meshroom/ui/qml/Controls/IntSelector.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import QtQuick 2.15
import MaterialIcons 2.2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.11

/*
* IntSelector with arrows and a text input to select a number
*/

Row {
id: root

property string tooltipText: ""
property int value: 0
property var range: { "min" : 0, "max" : 0 }

Layout.alignment: Qt.AlignVCenter

spacing: 0
property bool displayButtons: previousIntButton.hovered || intInputMouseArea.containsMouse || nextIntButton.hovered
property real buttonsOpacity: displayButtons ? 1.0 : 0.0

MaterialToolButton {
id: previousIntButton

opacity: buttonsOpacity
width: 10
text: MaterialIcons.navigate_before
ToolTip.text: "Previous"

onClicked: {
if (value > range.min) {
value -= 1
}
}
}

TextInput {
id: intInput

ToolTip.text: tooltipText
ToolTip.visible: tooltipText && intInputMouseArea.containsMouse

width: intMetrics.width
height: previousIntButton.height

color: palette.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
selectByMouse: true

text: value

onEditingFinished: {
// 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
value = parseInt(text)
value = Math.min(range.max, Math.max(range.min, parseInt(text)))
focus = false
}

MouseArea {
id: intInputMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}

MaterialToolButton {
id: nextIntButton

width: 10
opacity: buttonsOpacity
text: MaterialIcons.navigate_next
ToolTip.text: "Next"

onClicked: {
if (value < range.max) {
value += 1
}
}
}

TextMetrics {
id: intMetrics

font: intInput.font
text: "10000"
}

}
1 change: 1 addition & 0 deletions meshroom/ui/qml/Controls/qmldir
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ TabPanel 1.0 TabPanel.qml
TextFileViewer 1.0 TextFileViewer.qml
ExifOrientedViewer 1.0 ExifOrientedViewer.qml
FilterComboBox 1.0 FilterComboBox.qml
IntSelector 1.0 IntSelector.qml
1 change: 0 additions & 1 deletion meshroom/ui/qml/GraphEditor/AttributePin.qml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ RowLayout {
|| drag.source.objectName != inputDragTarget.objectName // not an edge connector
|| drag.source.baseType !== inputDragTarget.baseType // not the same base type
|| drag.source.nodeItem === inputDragTarget.nodeItem // connection between attributes of the same node
|| (drag.source.isList && !inputDragTarget.isList) // connection between a list and a simple attribute
|| (drag.source.isList && childrenRepeater.count) // source/target are lists but target already has children
|| 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)
) {
Expand Down
Loading
Loading