From d291fcc39dc3f3ba2247eb81e1ec4ef1f1cac40b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 14 Dec 2020 19:02:49 +0100 Subject: [PATCH 01/15] [core] graph: use new dependenciesOnly option on graph operations Input attribute to input attribute connection is not a real dependency between the nodes. --- meshroom/core/graph.py | 81 ++++++++++++++++++++++-------------- meshroom/core/node.py | 14 +++---- meshroom/core/taskManager.py | 6 +-- meshroom/ui/graph.py | 2 +- 4 files changed, 60 insertions(+), 43 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index a1843477..27d2e56e 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -85,9 +85,10 @@ class Visitor(object): Base class for Graph Visitors that does nothing. Sub-classes can override any method to implement specific algorithms. """ - def __init__(self, reverse): + def __init__(self, reverse, dependenciesOnly): super(Visitor, self).__init__() self.reverse = reverse + self.dependenciesOnly = dependenciesOnly # def initializeVertex(self, s, g): # '''is invoked on every vertex of the graph before the start of the graph search.''' @@ -383,7 +384,7 @@ class Graph(BaseObject): Returns: OrderedDict[Node, Node]: the source->duplicate map """ - srcNodes, srcEdges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True) + srcNodes, srcEdges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True, dependenciesOnly=True) # use OrderedDict to keep duplicated nodes creation order duplicates = OrderedDict() @@ -581,13 +582,13 @@ class Graph(BaseObject): def edge(self, dstAttributeName): return self._edges.get(dstAttributeName) - def getLeafNodes(self): - nodesWithOutput = set([edge.src.node for edge in self.edges]) - return set(self._nodes) - nodesWithOutput + def getLeafNodes(self, dependenciesOnly): + nodesWithOutputLink = set([edge.src.node for edge in self.getEdges(dependenciesOnly)]) + return set(self._nodes) - nodesWithOutputLink - def getRootNodes(self): - nodesWithInput = set([edge.dst.node for edge in self.edges]) - return set(self._nodes) - nodesWithInput + def getRootNodes(self, dependenciesOnly): + nodesWithInputLink = set([edge.dst.node for edge in self.getEdges(dependenciesOnly)]) + return set(self._nodes) - nodesWithInputLink @changeTopology def addEdge(self, srcAttr, dstAttr): @@ -635,21 +636,21 @@ class Graph(BaseObject): minDepth, maxDepth = self._nodesMinMaxDepths[node] return minDepth if minimal else maxDepth - def getInputEdges(self, node): - return set([edge for edge in self.edges if edge.dst.node is node]) + def getInputEdges(self, node, dependenciesOnly): + return set([edge for edge in self.getEdges(dependenciesOnly=dependenciesOnly) if edge.dst.node is node]) - def _getInputEdgesPerNode(self): + def _getInputEdgesPerNode(self, dependenciesOnly): nodeEdges = defaultdict(set) - for edge in self.edges: + for edge in self.getEdges(dependenciesOnly=dependenciesOnly): nodeEdges[edge.dst.node].add(edge.src.node) return nodeEdges - def _getOutputEdgesPerNode(self): + def _getOutputEdgesPerNode(self, dependenciesOnly): nodeEdges = defaultdict(set) - for edge in self.edges: + for edge in self.getEdges(dependenciesOnly=dependenciesOnly): nodeEdges[edge.src.node].add(edge.dst.node) return nodeEdges @@ -657,7 +658,7 @@ class Graph(BaseObject): def dfs(self, visitor, startNodes=None, longestPathFirst=False): # Default direction (visitor.reverse=False): from node to root # Reverse direction (visitor.reverse=True): from node to leaves - nodeChildren = self._getOutputEdgesPerNode() if visitor.reverse else self._getInputEdgesPerNode() + nodeChildren = self._getOutputEdgesPerNode(visitor.dependenciesOnly) if visitor.reverse else self._getInputEdgesPerNode(visitor.dependenciesOnly) # Initialize color map colors = {} for u in self._nodes: @@ -668,7 +669,7 @@ class Graph(BaseObject): # it is not possible to handle this case at the moment raise NotImplementedError("Graph.dfs(): longestPathFirst=True and visitor.reverse=True are not compatible yet.") - nodes = startNodes or (self.getRootNodes() if visitor.reverse else self.getLeafNodes()) + nodes = startNodes or (self.getRootNodes(visitor.dependenciesOnly) if visitor.reverse else self.getLeafNodes(visitor.dependenciesOnly)) if longestPathFirst: # Graph topology must be known and node depths up-to-date @@ -711,7 +712,7 @@ class Graph(BaseObject): colors[u] = BLACK visitor.finishVertex(u, self) - def dfsOnFinish(self, startNodes=None, longestPathFirst=False, reverse=False): + def dfsOnFinish(self, startNodes=None, longestPathFirst=False, reverse=False, dependenciesOnly=False): """ Return the node chain from startNodes to the graph roots/leaves. Order is defined by the visit and finishVertex event. @@ -728,13 +729,13 @@ class Graph(BaseObject): """ nodes = [] edges = [] - visitor = Visitor(reverse=reverse) + visitor = Visitor(reverse=reverse, dependenciesOnly=dependenciesOnly) visitor.finishVertex = lambda vertex, graph: nodes.append(vertex) visitor.finishEdge = lambda edge, graph: edges.append(edge) self.dfs(visitor=visitor, startNodes=startNodes, longestPathFirst=longestPathFirst) return nodes, edges - def dfsOnDiscover(self, startNodes=None, filterTypes=None, longestPathFirst=False, reverse=False): + def dfsOnDiscover(self, startNodes=None, filterTypes=None, longestPathFirst=False, reverse=False, dependenciesOnly=False): """ Return the node chain from startNodes to the graph roots/leaves. Order is defined by the visit and discoverVertex event. @@ -753,7 +754,7 @@ class Graph(BaseObject): """ nodes = [] edges = [] - visitor = Visitor(reverse=reverse) + visitor = Visitor(reverse=reverse, dependenciesOnly=dependenciesOnly) def discoverVertex(vertex, graph): if not filterTypes or vertex.nodeType in filterTypes: @@ -777,7 +778,7 @@ class Graph(BaseObject): """ nodes = [] edges = [] - visitor = Visitor(reverse=False) + visitor = Visitor(reverse=False, dependenciesOnly=True) def discoverVertex(vertex, graph): if vertex.hasStatus(Status.SUCCESS): @@ -832,7 +833,7 @@ class Graph(BaseObject): self._computationBlocked.clear() compatNodes = [] - visitor = Visitor(reverse=False) + visitor = Visitor(reverse=False, dependenciesOnly=True) def discoverVertex(vertex, graph): # initialize depths @@ -866,7 +867,7 @@ class Graph(BaseObject): # propagate inputVertex computability self._computationBlocked[currentVertex] |= self._computationBlocked[inputVertex] - leaves = self.getLeafNodes() + leaves = self.getLeafNodes(visitor.dependenciesOnly) visitor.finishEdge = finishEdge visitor.discoverVertex = discoverVertex self.dfs(visitor=visitor, startNodes=leaves) @@ -890,7 +891,7 @@ class Graph(BaseObject): """ nodesStack = [] edgesScore = defaultdict(lambda: 0) - visitor = Visitor(reverse=False) + visitor = Visitor(reverse=False, dependenciesOnly=False) def finishEdge(edge, graph): u, v = edge @@ -926,18 +927,34 @@ class Graph(BaseObject): flowEdges.append(link) return flowEdges - def getInputNodes(self, node, recursive=False): + def getEdges(self, dependenciesOnly=False): + if not dependenciesOnly: + return self.edges + + outEdges = [] + for e in self.edges: + attr = e.src + if dependenciesOnly: + if attr.isLink: + attr = attr.getLinkParam(recursive=True) + if not attr.isOutput: + continue + newE = Edge(attr, e.dst) + outEdges.append(newE) + return outEdges + + def getInputNodes(self, node, recursive, dependenciesOnly): """ Return either the first level input nodes of a node or the whole chain. """ if not recursive: - return set([edge.src.node for edge in self.edges if edge.dst.node is node]) + return set([edge.src.node for edge in self.getEdges(dependenciesOnly) if edge.dst.node is node]) inputNodes, edges = self.dfsOnDiscover(startNodes=[node], filterTypes=None, reverse=False) return inputNodes[1:] # exclude current node - def getOutputNodes(self, node, recursive=False): + def getOutputNodes(self, node, recursive, dependenciesOnly): """ Return either the first level output nodes of a node or the whole chain. """ if not recursive: - return set([edge.dst.node for edge in self.edges if edge.src.node is node]) + return set([edge.dst.node for edge in self.getEdges(dependenciesOnly) if edge.src.node is node]) outputNodes, edges = self.dfsOnDiscover(startNodes=[node], filterTypes=None, reverse=True) return outputNodes[1:] # exclude current node @@ -957,8 +974,8 @@ class Graph(BaseObject): return 0 class SCVisitor(Visitor): - def __init__(self, reverse): - super(SCVisitor, self).__init__(reverse) + def __init__(self, reverse, dependenciesOnly): + super(SCVisitor, self).__init__(reverse, dependenciesOnly) canCompute = True canSubmit = True @@ -969,7 +986,7 @@ class Graph(BaseObject): if vertex.isExtern(): self.canCompute = False - visitor = SCVisitor(reverse=False) + visitor = SCVisitor(reverse=False, dependenciesOnly=True) self.dfs(visitor=visitor, startNodes=[startNode]) return visitor.canCompute + (2 * visitor.canSubmit) @@ -1131,7 +1148,7 @@ class Graph(BaseObject): @Slot(Node) def clearDataFrom(self, startNode): - for node in self.dfsOnDiscover(startNodes=[startNode], reverse=True)[0]: + for node in self.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)[0]: node.clearData() def iterChunksByStatus(self, status): diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 7b06fd74..bba56b74 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -589,11 +589,11 @@ class BaseNode(BaseObject): def minDepth(self): return self.graph.getDepth(self, minimal=True) - def getInputNodes(self, recursive=False): - return self.graph.getInputNodes(self, recursive=recursive) + def getInputNodes(self, recursive, dependenciesOnly): + return self.graph.getInputNodes(self, recursive=recursive, dependenciesOnly=dependenciesOnly) - def getOutputNodes(self, recursive=False): - return self.graph.getOutputNodes(self, recursive=recursive) + def getOutputNodes(self, recursive, dependenciesOnly): + return self.graph.getOutputNodes(self, recursive=recursive, dependenciesOnly=dependenciesOnly) def toDict(self): pass @@ -883,7 +883,7 @@ class BaseNode(BaseObject): # Warning: we must handle some specific cases for global start/stop if self._locked and currentStatus in (Status.ERROR, Status.STOPPED, Status.NONE): self.setLocked(False) - inputNodes = self.getInputNodes(recursive=True) + inputNodes = self.getInputNodes(recursive=True, dependenciesOnly=True) for node in inputNodes: if node.getGlobalStatus() == Status.RUNNING: @@ -901,8 +901,8 @@ class BaseNode(BaseObject): if currentStatus == Status.SUCCESS: # At this moment, the node is necessarily locked because of previous if statement - inputNodes = self.getInputNodes(recursive=True) - outputNodes = self.getOutputNodes(recursive=True) + inputNodes = self.getInputNodes(recursive=True, dependenciesOnly=True) + outputNodes = self.getOutputNodes(recursive=True, dependenciesOnly=True) stayLocked = None # Check if at least one dependentNode is submitted or currently running diff --git a/meshroom/core/taskManager.py b/meshroom/core/taskManager.py index a2d092e0..bac031c5 100644 --- a/meshroom/core/taskManager.py +++ b/meshroom/core/taskManager.py @@ -147,7 +147,7 @@ class TaskManager(BaseObject): self.removeNode(node, displayList=False, processList=True) # Remove output nodes from display and computing lists - outputNodes = node.getOutputNodes(recursive=True) + outputNodes = node.getOutputNodes(recursive=True, dependenciesOnly=True) for n in outputNodes: if n.getGlobalStatus() in (Status.ERROR, Status.SUBMITTED): n.upgradeStatusTo(Status.NONE) @@ -184,7 +184,7 @@ class TaskManager(BaseObject): else: # Check dependencies of toNodes if not toNodes: - toNodes = graph.getLeafNodes() + toNodes = graph.getLeafNodes(dependenciesOnly=True) toNodes = list(toNodes) allReady = self.checkNodesDependencies(graph, toNodes, "COMPUTATION") @@ -402,7 +402,7 @@ class TaskManager(BaseObject): # Check dependencies of toNodes if not toNodes: - toNodes = graph.getLeafNodes() + toNodes = graph.getLeafNodes(dependenciesOnly=True) toNodes = list(toNodes) allReady = self.checkNodesDependencies(graph, toNodes, "SUBMITTING") diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 00dfa8d5..e33579cd 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -526,7 +526,7 @@ class UIGraph(QObject): with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)): # Perform nodes removal from leaves to start node so that edges # can be re-created in correct order on redo. - [self.removeNode(node) for node in reversed(self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True)[0])] + [self.removeNode(node) for node in reversed(self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)[0])] @Slot(Attribute, Attribute) def addEdge(self, src, dst): From b75ccdb02f9430e75d634607fa90328162338be3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 18 Dec 2020 01:09:34 +0100 Subject: [PATCH 02/15] [ui] GraphEditor: use dash lines for input/input edges --- meshroom/ui/qml/GraphEditor/Edge.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/qml/GraphEditor/Edge.qml b/meshroom/ui/qml/GraphEditor/Edge.qml index 730ab809..f5fea092 100644 --- a/meshroom/ui/qml/GraphEditor/Edge.qml +++ b/meshroom/ui/qml/GraphEditor/Edge.qml @@ -41,6 +41,8 @@ Shape { startY: root.startY fillColor: "transparent" strokeColor: "#3E3E3E" + strokeStyle: edge != undefined && ((edge.src != undefined && edge.src.isOutput) || edge.dst == undefined) ? ShapePath.SolidLine : ShapePath.DashLine + dashPattern: [4, 4] capStyle: ShapePath.RoundCap strokeWidth: 1 From ecf10a65f7060d72866428f10b6d39f936a66e81 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 18 Dec 2020 01:13:54 +0100 Subject: [PATCH 03/15] [ui] GraphEditor: visualize edges between params (non File attributes) --- meshroom/core/attribute.py | 2 +- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 16 +- meshroom/ui/qml/GraphEditor/Node.qml | 394 ++++++++++++-------- 3 files changed, 254 insertions(+), 158 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index ec408bca..1257b81d 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -137,7 +137,7 @@ class Attribute(BaseObject): self.valueChanged.emit() def resetValue(self): - self._value = "" + self._value = self.attributeDesc.value def requestGraphUpdate(self): if self.node.graph: diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 8acbc718..9b6c2e11 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -236,10 +236,10 @@ Item { model: nodeRepeater.loaded && root.graph ? root.graph.edges : undefined delegate: Edge { - property var src: edge ? root._attributeToDelegate[edge.src] : undefined - property var dst: edge ? root._attributeToDelegate[edge.dst] : undefined - property var srcAnchor: src.nodeItem.mapFromItem(src, src.outputAnchorPos.x, src.outputAnchorPos.y) - property var dstAnchor: dst.nodeItem.mapFromItem(dst, dst.inputAnchorPos.x, dst.inputAnchorPos.y) + property var src: root._attributeToDelegate[edge.src] + property var dst: root._attributeToDelegate[edge.dst] + property bool isValidEdge: src != undefined && dst != undefined + visible: isValidEdge property bool inFocus: containsMouse || (edgeMenu.opened && edgeMenu.currentEdge == edge) @@ -247,10 +247,10 @@ Item { color: inFocus ? activePalette.highlight : activePalette.text thickness: inFocus ? 2 : 1 opacity: 0.7 - point1x: src.nodeItem.x + srcAnchor.x - point1y: src.nodeItem.y + srcAnchor.y - point2x: dst.nodeItem.x + dstAnchor.x - point2y: dst.nodeItem.y + dstAnchor.y + point1x: isValidEdge ? src.globalX + src.outputAnchorPos.x : 0 + point1y: isValidEdge ? src.globalY + src.outputAnchorPos.y : 0 + point2x: isValidEdge ? dst.globalX + dst.inputAnchorPos.x : 0 + point2y: isValidEdge ? dst.globalY + dst.inputAnchorPos.y : 0 onPressed: { const canEdit = !edge.dst.node.locked diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index fb281659..ace25034 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -27,6 +27,11 @@ Item { readonly property color defaultColor: isCompatibilityNode ? "#444" : activePalette.base property color baseColor: defaultColor + Item { + id: m + property bool displayParams: false + } + // Mouse interaction related signals signal pressed(var mouse) signal doubleClicked(var mouse) @@ -60,7 +65,7 @@ Item { } // Whether an attribute can be displayed as an attribute pin on the node - function isDisplayableAsPin(attribute) { + function isFileAttributeBaseType(attribute) { // ATM, only File attributes are meant to be connected // TODO: review this if we want to connect something else return attribute.type == "File" @@ -110,7 +115,7 @@ Item { // Selection border Rectangle { - anchors.fill: parent + anchors.fill: nodeContent anchors.margins: -border.width visible: root.selected || root.hovered border.width: 2.5 @@ -120,10 +125,9 @@ Item { color: "transparent" } - // Background Rectangle { id: background - anchors.fill: parent + anchors.fill: nodeContent color: Qt.lighter(activePalette.base, 1.4) layer.enabled: true layer.effect: DropShadow { radius: 3; color: shadowColor } @@ -131,192 +135,284 @@ Item { opacity: 0.7 } - // Data Layout - Column { - id: body + Rectangle { + id: nodeContent width: parent.width + height: childrenRect.height + color: "transparent" - // Header - Rectangle { - id: header + // Data Layout + Column { + id: body width: parent.width - height: headerLayout.height - color: root.selected ? activePalette.highlight : root.baseColor - radius: background.radius - // Fill header's bottom radius + // Header Rectangle { + id: header width: parent.width - height: parent.radius - anchors.bottom: parent.bottom - color: parent.color - z: -1 - } + height: headerLayout.height + color: root.selected ? activePalette.highlight : root.baseColor + radius: background.radius - // Header Layout - RowLayout { - id: headerLayout - width: parent.width - spacing: 0 - - // Node Name - Label { - Layout.fillWidth: true - text: node ? node.label : "" - padding: 4 - color: root.selected ? "white" : activePalette.text - elide: Text.ElideMiddle - font.pointSize: 8 + // Fill header's bottom radius + Rectangle { + width: parent.width + height: parent.radius + anchors.bottom: parent.bottom + color: parent.color + z: -1 } - // Node State icons + // Header Layout RowLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignRight - Layout.rightMargin: 2 - spacing: 2 + id: headerLayout + width: parent.width + spacing: 0 - // CompatibilityBadge icon for CompatibilityNodes - Loader { - active: root.isCompatibilityNode - sourceComponent: CompatibilityBadge { - sourceComponent: iconDelegate - canUpgrade: root.node.canUpgrade - issueDetails: root.node.issueDetails + // Node Name + Label { + Layout.fillWidth: true + text: node ? node.label : "" + padding: 4 + color: root.selected ? "white" : activePalette.text + elide: Text.ElideMiddle + font.pointSize: 8 + } + + // Node State icons + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + Layout.rightMargin: 2 + spacing: 2 + + // CompatibilityBadge icon for CompatibilityNodes + Loader { + active: root.isCompatibilityNode + sourceComponent: CompatibilityBadge { + sourceComponent: iconDelegate + canUpgrade: root.node.canUpgrade + issueDetails: root.node.issueDetails + } } - } - // Data sharing indicator - // Note: for an unknown reason, there are some performance issues with the UI refresh. - // Example: a node duplicated 40 times will be slow while creating another identical node - // (sharing the same uid) will not be as slow. If save, quit and reload, it will become slow. - MaterialToolButton { - property string baseText: "Shares internal folder (data) with other node(s). Hold click for details." - property string toolTipText: visible ? baseText : "" - visible: node.hasDuplicates - text: MaterialIcons.layers - font.pointSize: 7 - padding: 2 - palette.text: Colors.sysPalette.text - ToolTip.text: toolTipText + // Data sharing indicator + // Note: for an unknown reason, there are some performance issues with the UI refresh. + // Example: a node duplicated 40 times will be slow while creating another identical node + // (sharing the same uid) will not be as slow. If save, quit and reload, it will become slow. + MaterialToolButton { + property string baseText: "Shares internal folder (data) with other node(s). Hold click for details." + property string toolTipText: visible ? baseText : "" + visible: node.hasDuplicates + text: MaterialIcons.layers + font.pointSize: 7 + padding: 2 + palette.text: Colors.sysPalette.text + ToolTip.text: toolTipText - onPressed: { offsetReleased.running = false; toolTipText = visible ? generateDuplicateList() : "" } - onReleased: { toolTipText = "" ; offsetReleased.running = true } - onCanceled: released() + onPressed: { offsetReleased.running = false; toolTipText = visible ? generateDuplicateList() : "" } + onReleased: { toolTipText = "" ; offsetReleased.running = true } + onCanceled: released() - // Used for a better user experience with the button - // Avoid to change the text too quickly - Timer { - id: offsetReleased - interval: 750; running: false; repeat: false - onTriggered: parent.toolTipText = visible ? parent.baseText : "" + // Used for a better user experience with the button + // Avoid to change the text too quickly + Timer { + id: offsetReleased + interval: 750; running: false; repeat: false + onTriggered: parent.toolTipText = visible ? parent.baseText : "" + } } - } - // Submitted externally indicator - MaterialLabel { - visible: ["SUBMITTED", "RUNNING"].includes(node.globalStatus) && node.chunks.count > 0 && node.globalExecMode === "EXTERN" - text: MaterialIcons.cloud - padding: 2 - font.pointSize: 7 - palette.text: Colors.sysPalette.text - ToolTip.text: "Computed Externally" - } + // Submitted externally indicator + MaterialLabel { + visible: ["SUBMITTED", "RUNNING"].includes(node.globalStatus) && node.chunks.count > 0 && node.globalExecMode === "EXTERN" + text: MaterialIcons.cloud + padding: 2 + font.pointSize: 7 + palette.text: Colors.sysPalette.text + ToolTip.text: "Computed Externally" + } - // Lock indicator - MaterialLabel { - visible: root.readOnly - text: MaterialIcons.lock - padding: 2 - font.pointSize: 7 - palette.text: "red" - ToolTip.text: "Locked" + // Lock indicator + MaterialLabel { + visible: root.readOnly + text: MaterialIcons.lock + padding: 2 + font.pointSize: 7 + palette.text: "red" + ToolTip.text: "Locked" + } } } } - } - // Node Chunks - NodeChunks { - defaultColor: Colors.sysPalette.mid - implicitHeight: 3 - width: parent.width - model: node ? node.chunks : undefined + // Node Chunks + NodeChunks { + defaultColor: Colors.sysPalette.mid + implicitHeight: 3 + width: parent.width + model: node ? node.chunks : undefined - Rectangle { - anchors.fill: parent - color: Colors.sysPalette.mid - z: -1 + Rectangle { + anchors.fill: parent + color: Colors.sysPalette.mid + z: -1 + } } - } - // Vertical Spacer - Item { width: parent.width; height: 2 } + // Vertical Spacer + Item { width: parent.width; height: 2 } - // Input/Output Attributes - Item { - id: nodeAttributes - width: parent.width - 2 - height: childrenRect.height - anchors.horizontalCenter: parent.horizontalCenter + // Input/Output Attributes + Item { + id: nodeAttributes + width: parent.width - 2 + height: childrenRect.height + anchors.horizontalCenter: parent.horizontalCenter - enabled: !root.readOnly && !root.isCompatibilityNode - - Column { - width: parent.width - spacing: 5 - bottomPadding: 2 + enabled: !root.readOnly && !root.isCompatibilityNode Column { - id: outputs + id: attributesColumn width: parent.width - spacing: 3 - Repeater { - model: node ? node.attributes : undefined + spacing: 5 + bottomPadding: 2 - delegate: Loader { - id: outputLoader - active: object.isOutput && isDisplayableAsPin(object) - anchors.right: parent.right - width: outputs.width + Column { + id: outputs + width: parent.width + spacing: 3 + Repeater { + model: node ? node.attributes : undefined - sourceComponent: AttributePin { - id: outPin - nodeItem: root - attribute: object + delegate: Loader { + id: outputLoader + active: object.isOutput && isFileAttributeBaseType(object) + anchors.right: parent.right + width: outputs.width - readOnly: root.readOnly - onPressed: root.pressed(mouse) - Component.onCompleted: attributePinCreated(object, outPin) - Component.onDestruction: attributePinDeleted(attribute, outPin) + sourceComponent: AttributePin { + id: outPin + nodeItem: root + attribute: object + + property real globalX: root.x + nodeAttributes.x + outputs.x + outputLoader.x + outPin.x + property real globalY: root.y + nodeAttributes.y + outputs.y + outputLoader.y + outPin.y + + readOnly: root.readOnly + onPressed: root.pressed(mouse) + Component.onCompleted: attributePinCreated(object, outPin) + Component.onDestruction: attributePinDeleted(attribute, outPin) + } } } } - } - Column { - id: inputs - width: parent.width - spacing: 3 - Repeater { - model: node ? node.attributes : undefined - delegate: Loader { - active: !object.isOutput && isDisplayableAsPin(object) - width: inputs.width + Column { + id: inputs + width: parent.width + spacing: 3 + Repeater { + model: node ? node.attributes : undefined + delegate: Loader { + id: inputLoader + active: !object.isOutput && isFileAttributeBaseType(object) + width: inputs.width - sourceComponent: AttributePin { - id: inPin - nodeItem: root - attribute: object - readOnly: root.readOnly - Component.onCompleted: attributePinCreated(attribute, inPin) - Component.onDestruction: attributePinDeleted(attribute, inPin) - onPressed: root.pressed(mouse) - onChildPinCreated: attributePinCreated(childAttribute, inPin) - onChildPinDeleted: attributePinDeleted(childAttribute, inPin) + sourceComponent: AttributePin { + id: inPin + nodeItem: root + attribute: object + + property real globalX: root.x + nodeAttributes.x + inputs.x + inputLoader.x + inPin.x + property real globalY: root.y + nodeAttributes.y + inputs.y + inputLoader.y + inPin.y + + readOnly: root.readOnly + Component.onCompleted: attributePinCreated(attribute, inPin) + Component.onDestruction: attributePinDeleted(attribute, inPin) + onPressed: root.pressed(mouse) + onChildPinCreated: attributePinCreated(childAttribute, inPin) + onChildPinDeleted: attributePinDeleted(childAttribute, inPin) + } } } } + + // Vertical Spacer + Rectangle { + height: inputParams.height > 0 ? 3 : 0 + visible: (height == 3) + Behavior on height { PropertyAnimation {easing.type: Easing.Linear} } + width: parent.width + color: Colors.sysPalette.mid + MaterialToolButton { + text: " " + width: parent.width + height: parent.height + padding: 0 + spacing: 0 + anchors.margins: 0 + font.pointSize: 6 + onClicked: { + m.displayParams = ! m.displayParams + } + } + } + + Rectangle { + id: inputParamsRect + width: parent.width + height: childrenRect.height + color: "transparent" + + Column { + id: inputParams + width: parent.width + spacing: 3 + Repeater { + id: inputParamsRepeater + model: node ? node.attributes : undefined + delegate: Loader { + id: paramLoader + active: !object.isOutput && !isFileAttributeBaseType(object) + property bool isFullyActive: (m.displayParams || object.isLink || object.hasOutputConnections) + width: parent.width + + sourceComponent: AttributePin { + id: inPin + nodeItem: root + property real globalX: root.x + nodeAttributes.x + inputParamsRect.x + paramLoader.x + inPin.x + property real globalY: root.y + nodeAttributes.y + inputParamsRect.y + paramLoader.y + inPin.y + + height: isFullyActive ? childrenRect.height : 0 + Behavior on height { PropertyAnimation {easing.type: Easing.Linear} } + visible: (height == childrenRect.height) + attribute: object + readOnly: root.readOnly + Component.onCompleted: attributePinCreated(attribute, inPin) + Component.onDestruction: attributePinDeleted(attribute, inPin) + onPressed: root.pressed(mouse) + onChildPinCreated: attributePinCreated(childAttribute, inPin) + onChildPinDeleted: attributePinDeleted(childAttribute, inPin) + } + } + } + } + } + + MaterialToolButton { + text: root.hovered ? (m.displayParams ? MaterialIcons.arrow_drop_up : MaterialIcons.arrow_drop_down) : " " + Layout.alignment: Qt.AlignBottom + width: parent.width + height: 5 + padding: 0 + spacing: 0 + anchors.margins: 0 + font.pointSize: 10 + onClicked: { + m.displayParams = ! m.displayParams + } + } } } } From 3fe6c22bc2b01b348b9d76d0a2307ccbde71f9ba Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 18 Dec 2020 01:15:12 +0100 Subject: [PATCH 04/15] [multiview] Add connections between params in default pipelines --- meshroom/multiview.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 616e4d04..48b0e4b9 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -210,10 +210,12 @@ def panoramaHdrPipeline(graph): ldr2hdrCalibration = graph.addNewNode('LdrToHdrCalibration', input=ldr2hdrSampling.input, + userNbBrackets=ldr2hdrSampling.userNbBrackets, samples=ldr2hdrSampling.output) ldr2hdrMerge = graph.addNewNode('LdrToHdrMerge', input=ldr2hdrCalibration.input, + userNbBrackets=ldr2hdrCalibration.userNbBrackets, response=ldr2hdrCalibration.response) featureExtraction = graph.addNewNode('FeatureExtraction', @@ -233,12 +235,14 @@ def panoramaHdrPipeline(graph): featureMatching = graph.addNewNode('FeatureMatching', input=imageMatching.input, featuresFolders=imageMatching.featuresFolders, - imagePairsList=imageMatching.output) + imagePairsList=imageMatching.output, + describerTypes=featureExtraction.describerTypes) panoramaEstimation = graph.addNewNode('PanoramaEstimation', - input=featureMatching.input, - featuresFolders=featureMatching.featuresFolders, - matchesFolders=[featureMatching.output]) + input=featureMatching.input, + featuresFolders=featureMatching.featuresFolders, + matchesFolders=[featureMatching.output], + describerTypes=featureMatching.describerTypes) panoramaOrientation = graph.addNewNode('SfMTransform', input=panoramaEstimation.output, @@ -340,11 +344,13 @@ def sfmPipeline(graph): featureMatching = graph.addNewNode('FeatureMatching', input=imageMatching.input, featuresFolders=imageMatching.featuresFolders, - imagePairsList=imageMatching.output) + imagePairsList=imageMatching.output, + describerTypes=featureExtraction.describerTypes) structureFromMotion = graph.addNewNode('StructureFromMotion', input=featureMatching.input, featuresFolders=featureMatching.featuresFolders, - matchesFolders=[featureMatching.output]) + matchesFolders=[featureMatching.output], + describerTypes=featureMatching.describerTypes) return [ cameraInit, featureExtraction, @@ -419,16 +425,18 @@ def sfmAugmentation(graph, sourceSfm, withMVS=False): featureMatching = graph.addNewNode('FeatureMatching', input=imageMatchingMulti.outputCombinedSfM, featuresFolders=imageMatchingMulti.featuresFolders, - imagePairsList=imageMatchingMulti.output) + imagePairsList=imageMatchingMulti.output, + describerTypes=featureExtraction.describerTypes) structureFromMotion = graph.addNewNode('StructureFromMotion', input=featureMatching.input, featuresFolders=featureMatching.featuresFolders, - matchesFolders=[featureMatching.output]) + matchesFolders=[featureMatching.output], + describerTypes=featureMatching.describerTypes) graph.addEdge(sourceSfm.output, imageMatchingMulti.inputB) sfmNodes = [ cameraInit, - featureMatching, + featureExtraction, imageMatchingMulti, featureMatching, structureFromMotion From 85044e50cb19d0412875c3561ddca7f2f6ceec96 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Dec 2020 12:01:17 +0100 Subject: [PATCH 05/15] [ui] GraphEditor: improve dash pattern on edges - Avoid variation in dash pattern when the stroke is hovered by compensating strokeWidth. - Use a dash pattern of [6,4] to get a stronger feeling of a dashed line (VS a dotted line). --- meshroom/ui/qml/GraphEditor/Edge.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/Edge.qml b/meshroom/ui/qml/GraphEditor/Edge.qml index f5fea092..e28a3f18 100644 --- a/meshroom/ui/qml/GraphEditor/Edge.qml +++ b/meshroom/ui/qml/GraphEditor/Edge.qml @@ -42,9 +42,11 @@ Shape { fillColor: "transparent" strokeColor: "#3E3E3E" strokeStyle: edge != undefined && ((edge.src != undefined && edge.src.isOutput) || edge.dst == undefined) ? ShapePath.SolidLine : ShapePath.DashLine - dashPattern: [4, 4] - capStyle: ShapePath.RoundCap strokeWidth: 1 + // final visual width of this path (never below 1) + readonly property real visualWidth: Math.max(strokeWidth, 1) + dashPattern: [6/visualWidth, 4/visualWidth] + capStyle: ShapePath.RoundCap PathCubic { id: cubic From 5fb6a5fb0f0907aa4bceff29cb044bf4aa832647 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Dec 2020 15:29:10 +0100 Subject: [PATCH 06/15] [ui] GraphEditor: only connect compatible attributes --- meshroom/core/attribute.py | 8 ++++ meshroom/ui/commands.py | 3 ++ meshroom/ui/qml/GraphEditor/AttributePin.qml | 42 +++++++++++--------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 1257b81d..c0651de2 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -93,6 +93,9 @@ class Attribute(BaseObject): def getType(self): return self.attributeDesc.__class__.__name__ + def getBaseType(self): + return self.getType() + def getLabel(self): return self._label @@ -258,6 +261,7 @@ class Attribute(BaseObject): fullName = Property(str, getFullName, constant=True) label = Property(str, getLabel, constant=True) type = Property(str, getType, constant=True) + baseType = Property(str, getType, constant=True) desc = Property(desc.Attribute, lambda self: self.attributeDesc, constant=True) valueChanged = Signal() value = Property(Variant, _get_value, _set_value, notify=valueChanged) @@ -292,6 +296,9 @@ class ListAttribute(Attribute): def __len__(self): return len(self._value) + def getBaseType(self): + return self.attributeDesc.elementDesc.__class__.__name__ + def at(self, idx): """ Returns child attribute at index 'idx' """ # implement 'at' rather than '__getitem__' @@ -396,6 +403,7 @@ class ListAttribute(Attribute): # Override value property setter value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged) isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged) + baseType = Property(str, getBaseType, constant=True) class GroupAttribute(Attribute): diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 32cf85df..f5429c0e 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -227,6 +227,9 @@ class AddEdgeCommand(GraphCommand): self.dstAttr = dst.getFullName() self.setText("Connect '{}'->'{}'".format(self.srcAttr, self.dstAttr)) + if src.baseType != dst.baseType: + raise ValueError("Attribute types are not compatible and cannot be connected: '{}'({})->'{}'({})".format(self.srcAttr, src.baseType, self.dstAttr, dst.baseType)) + def redoImpl(self): self.graph.addEdge(self.graph.attribute(self.srcAttr), self.graph.attribute(self.dstAttr)) return True diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index a51c8ac5..0e87ba41 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -82,16 +82,18 @@ RowLayout { keys: [inputDragTarget.objectName] onEntered: { - // Filter drops: - if( root.readOnly - || drag.source.objectName != inputDragTarget.objectName // not an edge connector - || drag.source.nodeItem == inputDragTarget.nodeItem // connection between attributes of the same node - || inputDragTarget.attribute.isLink // already connected attribute - || (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" - ) + // Check if attributes are compatible to create a valid connection + if( root.readOnly // cannot connect on a read-only attribute + || 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 + || inputDragTarget.attribute.isLink // already connected attribute + || (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) + ) { + // Refuse attributes connection drag.accepted = false } inputDropArea.acceptableDrop = drag.accepted @@ -112,7 +114,8 @@ RowLayout { readonly property string connectorType: "input" readonly property alias attribute: root.attribute readonly property alias nodeItem: root.nodeItem - readonly property bool isOutput: attribute && attribute.isOutput + readonly property bool isOutput: attribute.isOutput + readonly property string baseType: attribute.baseType readonly property alias isList: root.isList property bool dragAccepted: false anchors.verticalCenter: parent.verticalCenter @@ -219,15 +222,17 @@ RowLayout { keys: [outputDragTarget.objectName] onEntered: { - // Filter drops: - if( drag.source.objectName != outputDragTarget.objectName // not an edge connector - || drag.source.nodeItem == outputDragTarget.nodeItem // connection between attributes of the same node - || drag.source.attribute.isLink // already connected attribute - || (!drag.source.isList && outputDragTarget.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 == "output" - ) + // Check if attributes are compatible to create a valid connection + if( drag.source.objectName != outputDragTarget.objectName // not an edge connector + || drag.source.baseType != outputDragTarget.baseType // not the same base type + || drag.source.nodeItem == outputDragTarget.nodeItem // connection between attributes of the same node + || drag.source.attribute.isLink // already connected attribute + || (!drag.source.isList && outputDragTarget.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 == "output" // refuse to connect an output pin on another one + ) { + // Refuse attributes connection drag.accepted = false } outputDropArea.acceptableDrop = drag.accepted @@ -249,6 +254,7 @@ RowLayout { readonly property alias nodeItem: root.nodeItem readonly property bool isOutput: attribute.isOutput readonly property alias isList: root.isList + readonly property string baseType: attribute.baseType property bool dropAccepted: false anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter From d3d17ec6dda363dc40a5cec14bd4365bdebd964b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Dec 2020 17:31:48 +0100 Subject: [PATCH 07/15] [core] node: fix missing function argument to getInputNodes --- meshroom/core/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index bba56b74..00646e6c 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -918,7 +918,7 @@ class BaseNode(BaseObject): return elif currentStatus in lockedStatus and self._chunks.at(0).statusNodeName == self.name: self.setLocked(True) - inputNodes = self.getInputNodes(recursive=True) + inputNodes = self.getInputNodes(recursive=True, dependenciesOnly=True) for node in inputNodes: node.setLocked(True) return From 2b48188b3222425de5beb625e6c9f84dd66de4cb Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Dec 2020 17:34:32 +0100 Subject: [PATCH 08/15] [multiview] connect common attributes in LdrToHdr* nodes --- meshroom/multiview.py | 4 +++ .../nodes/aliceVision/LdrToHdrCalibration.py | 34 +++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 48b0e4b9..76ccce6c 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -211,11 +211,15 @@ def panoramaHdrPipeline(graph): ldr2hdrCalibration = graph.addNewNode('LdrToHdrCalibration', input=ldr2hdrSampling.input, userNbBrackets=ldr2hdrSampling.userNbBrackets, + byPass=ldr2hdrSampling.byPass, + channelQuantizationPower=ldr2hdrSampling.channelQuantizationPower, samples=ldr2hdrSampling.output) ldr2hdrMerge = graph.addNewNode('LdrToHdrMerge', input=ldr2hdrCalibration.input, userNbBrackets=ldr2hdrCalibration.userNbBrackets, + byPass=ldr2hdrCalibration.byPass, + channelQuantizationPower=ldr2hdrCalibration.channelQuantizationPower, response=ldr2hdrCalibration.response) featureExtraction = graph.addNewNode('FeatureExtraction', diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index 1be2cdd2..2c16734e 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -49,6 +49,23 @@ class LdrToHdrCalibration(desc.CommandLineNode): value=desc.Node.internalFolder, uid=[0], ), + desc.IntParam( + name='userNbBrackets', + label='Number of Brackets', + description='Number of exposure brackets per HDR image (0 for automatic detection).', + value=0, + range=(0, 15, 1), + uid=[], + group='user', # not used directly on the command line + ), + desc.IntParam( + name='nbBrackets', + label='Automatic Nb Brackets', + description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', + value=0, + range=(0, 10, 1), + uid=[0], + ), desc.BoolParam( name='byPass', label='Bypass', @@ -87,23 +104,6 @@ class LdrToHdrCalibration(desc.CommandLineNode): uid=[0], enabled= lambda node: node.byPass.enabled and not node.byPass.value, ), - desc.IntParam( - name='userNbBrackets', - label='Number of Brackets', - description='Number of exposure brackets per HDR image (0 for automatic detection).', - value=0, - range=(0, 15, 1), - uid=[], - group='user', # not used directly on the command line - ), - desc.IntParam( - name='nbBrackets', - label='Automatic Nb Brackets', - description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', - value=0, - range=(0, 10, 1), - uid=[0], - ), desc.IntParam( name='channelQuantizationPower', label='Channel Quantization Power', From 8ef793a488a7a17190c1f79b132b4e5be6833209 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Dec 2020 19:01:36 +0100 Subject: [PATCH 09/15] [ui] GraphEditor: output attributes are never read-only Also allow to expand attributes list on a locked node. --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 5 +++-- meshroom/ui/qml/GraphEditor/Node.qml | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 0e87ba41..d7db4cfb 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -155,7 +155,7 @@ RowLayout { point1y: inputDragTarget.y + inputDragTarget.height/2 point2x: parent.width / 2 point2y: parent.width / 2 - color: nameLabel.color + color: palette.highlight thickness: outputDragTarget.dropAccepted ? 2 : 1 } } @@ -171,6 +171,7 @@ RowLayout { Label { id: nameLabel + enabled: !root.readOnly property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active || inputDropArea.containsDrag || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag) text: attribute ? attribute.label : "" elide: hovered ? Text.ElideNone : Text.ElideMiddle @@ -289,7 +290,7 @@ RowLayout { point1y: parent.width / 2 point2x: outputDragTarget.x + outputDragTarget.width/2 point2y: outputDragTarget.y + outputDragTarget.height/2 - color: nameLabel.color + color: palette.highlight thickness: outputDragTarget.dropAccepted ? 2 : 1 } } diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index ace25034..2825dbbc 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -270,7 +270,7 @@ Item { height: childrenRect.height anchors.horizontalCenter: parent.horizontalCenter - enabled: !root.readOnly && !root.isCompatibilityNode + enabled: !root.isCompatibilityNode Column { id: attributesColumn @@ -299,7 +299,6 @@ Item { property real globalX: root.x + nodeAttributes.x + outputs.x + outputLoader.x + outPin.x property real globalY: root.y + nodeAttributes.y + outputs.y + outputLoader.y + outPin.y - readOnly: root.readOnly onPressed: root.pressed(mouse) Component.onCompleted: attributePinCreated(object, outPin) Component.onDestruction: attributePinDeleted(attribute, outPin) From bdb0cadc39942e6ae5da4e9f4c7139658252c5c4 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 30 Dec 2020 18:40:43 +0100 Subject: [PATCH 10/15] [ui] graph: fix missing argument --- meshroom/ui/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index e33579cd..434e0127 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -411,7 +411,7 @@ class UIGraph(QObject): node.clearSubmittedChunks() self._taskManager.removeNode(node, displayList=True, processList=True) - for n in node.getOutputNodes(recursive=True): + for n in node.getOutputNodes(recursive=True, dependenciesOnly=True): n.clearSubmittedChunks() self._taskManager.removeNode(n, displayList=True, processList=True) From 4ce295b348e4e8d859930cc901ebec76e26ec2f6 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 31 Dec 2020 13:44:36 +0100 Subject: [PATCH 11/15] [ui] Viewer2D: change wording in comments --- meshroom/ui/qml/Viewer/Viewer2D.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 79f731b3..262e3233 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -240,8 +240,7 @@ FocusScope { } // Image cache of the last loaded image - // Only visible when the main one is loading, to keep an image - // displayed at all time and smoothen transitions + // Only visible when the main one is loading, to maintain a displayed image for smoother transitions Image { id: qtImageViewerCache From a11b5d51aabf1307283d9539f9b275acc074d6b2 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 31 Dec 2020 14:10:05 +0100 Subject: [PATCH 12/15] [ui] Viewer2D: add a summary to the image metadata tab --- meshroom/ui/qml/Viewer/ImageMetadataView.qml | 47 +++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer/ImageMetadataView.qml b/meshroom/ui/qml/Viewer/ImageMetadataView.qml index d35ef2d0..7c86767f 100644 --- a/meshroom/ui/qml/Viewer/ImageMetadataView.qml +++ b/meshroom/ui/qml/Viewer/ImageMetadataView.qml @@ -129,7 +129,52 @@ FloatingPane { id: searchBar Layout.fillWidth: true } - + RowLayout { + Layout.alignment: Qt.AlignHCenter + Label { + font.family: MaterialIcons.fontFamily + text: MaterialIcons.shutter_speed + } + Label { + id: exposureLabel + text: { + if(metadata["ExposureTime"] === undefined) + return ""; + var expStr = metadata["ExposureTime"]; + var exp = parseFloat(expStr); + if(exp < 1.0) + { + var invExp = 1.0 / exp; + return "1/" + invExp.toFixed(0); + } + return expStr; + } + elide: Text.ElideRight + horizontalAlignment: Text.AlignHLeft + } + Item { width: 4 } + Label { + font.family: MaterialIcons.fontFamily + text: MaterialIcons.camera + } + Label { + id: fnumberLabel + text: (metadata["FNumber"] !== undefined) ? ("f/" + metadata["FNumber"]) : "" + elide: Text.ElideRight + horizontalAlignment: Text.AlignHLeft + } + Item { width: 4 } + Label { + font.family: MaterialIcons.fontFamily + text: MaterialIcons.iso + } + Label { + id: isoLabel + text: metadata["Exif:ISOSpeedRatings"] || "" + elide: Text.ElideRight + horizontalAlignment: Text.AlignHLeft + } + } // Metadata ListView ListView { id: metadataView From e3b416026d9affc40f4bf0e5067d32ea85157503 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 31 Dec 2020 14:11:32 +0100 Subject: [PATCH 13/15] [nodes] PanoramaWarping: fix missing Camel Case in label --- meshroom/nodes/aliceVision/PanoramaWarping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PanoramaWarping.py b/meshroom/nodes/aliceVision/PanoramaWarping.py index 0bcb646e..37642eac 100644 --- a/meshroom/nodes/aliceVision/PanoramaWarping.py +++ b/meshroom/nodes/aliceVision/PanoramaWarping.py @@ -44,7 +44,7 @@ Compute the image warping for each input image in the panorama coordinate system ), desc.IntParam( name='percentUpscale', - label='Upscale ratio', + label='Upscale Ratio', description='Percentage of upscaled pixels.\n' '\n' 'How many percent of the pixels will be upscaled (compared to its original resolution):\n' From d1d78e9a8b1a2c17c5a1731c1f5827add61d3eb1 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 4 Jan 2021 13:47:34 +0100 Subject: [PATCH 14/15] [ui] Viewer2D: add check to getImageFile --- meshroom/ui/qml/Viewer/Viewer2D.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 262e3233..480deaf0 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -123,6 +123,8 @@ FocusScope { } function getImageFile(type) { + if(!_reconstruction.activeNodes) + return ""; var depthMapNode = _reconstruction.activeNodes.get('allDepthMap').node; if (type == "image") { return root.source; From a17bc3d5f4581b72059f5fcf02bcaa614e440046 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 4 Jan 2021 13:49:14 +0100 Subject: [PATCH 15/15] [ui] graph: minor code change for readability --- meshroom/ui/graph.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 434e0127..73aed58c 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -524,9 +524,11 @@ class UIGraph(QObject): startNode (Node): the node to start from. """ with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)): + nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True) # Perform nodes removal from leaves to start node so that edges # can be re-created in correct order on redo. - [self.removeNode(node) for node in reversed(self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)[0])] + for node in reversed(nodes): + self.removeNode(node) @Slot(Attribute, Attribute) def addEdge(self, src, dst):