diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index ec408bca0d..c0651de274 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -93,6 +93,9 @@ def getName(self): def getType(self): return self.attributeDesc.__class__.__name__ + def getBaseType(self): + return self.getType() + def getLabel(self): return self._label @@ -137,7 +140,7 @@ def _set_value(self, value): self.valueChanged.emit() def resetValue(self): - self._value = "" + self._value = self.attributeDesc.value def requestGraphUpdate(self): if self.node.graph: @@ -258,6 +261,7 @@ def updateInternals(self): 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 @@ def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): 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 @@ def updateInternals(self): # 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/core/graph.py b/meshroom/core/graph.py index a1843477d3..27d2e56e94 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 @@ def duplicateNodesFromNode(self, fromNode): 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 @@ def findNodes(self, nodesExpr): 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 @@ def getDepth(self, node, minimal=False): 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 @@ def _getOutputEdgesPerNode(self): 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 @@ def dfs(self, visitor, startNodes=None, longestPathFirst=False): # 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 @@ def _dfsVisit(self, u, visitor, colors, nodeChildren, longestPathFirst): 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 @@ def dfsOnFinish(self, startNodes=None, longestPathFirst=False, reverse=False): """ 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 @@ def dfsOnDiscover(self, startNodes=None, filterTypes=None, longestPathFirst=Fals """ 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 @@ def dfsToProcess(self, startNodes=None): """ 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 @@ def updateNodesTopologicalData(self): self._computationBlocked.clear() compatNodes = [] - visitor = Visitor(reverse=False) + visitor = Visitor(reverse=False, dependenciesOnly=True) def discoverVertex(vertex, graph): # initialize depths @@ -866,7 +867,7 @@ def finishEdge(edge, graph): # 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 @@ def dfsMaxEdgeLength(self, startNodes=None): """ 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 @@ def flowEdges(self, startNodes=None): 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 @@ def canSubmitOrCompute(self, startNode): 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 @@ def discoverVertex(self, vertex, graph): 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 @@ def clearSubmittedNodes(self): @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 7b06fd749c..00646e6c6e 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -589,11 +589,11 @@ def depth(self): 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 @@ def updateLocked(self): # 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 @@ def updateLocked(self): 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 @@ -918,7 +918,7 @@ def updateLocked(self): 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 diff --git a/meshroom/core/taskManager.py b/meshroom/core/taskManager.py index a2d092e01e..bac031c527 100644 --- a/meshroom/core/taskManager.py +++ b/meshroom/core/taskManager.py @@ -147,7 +147,7 @@ def restart(self): 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 @@ def compute(self, graph=None, toNodes=None, forceCompute=False, forceStatus=Fals 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 @@ def submit(self, graph=None, submitter=None, toNodes=None): # 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/multiview.py b/meshroom/multiview.py index 616e4d044d..76ccce6ca8 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -210,10 +210,16 @@ 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', @@ -233,12 +239,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 +348,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 +429,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 diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index 1be2cdd231..2c16734e77 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', diff --git a/meshroom/nodes/aliceVision/PanoramaWarping.py b/meshroom/nodes/aliceVision/PanoramaWarping.py index 0bcb646e3a..37642eacfb 100644 --- a/meshroom/nodes/aliceVision/PanoramaWarping.py +++ b/meshroom/nodes/aliceVision/PanoramaWarping.py @@ -44,7 +44,7 @@ class PanoramaWarping(desc.CommandLineNode): ), 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' diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 32cf85df6c..f5429c0ea0 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -227,6 +227,9 @@ def __init__(self, graph, src, dst, parent=None): 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/graph.py b/meshroom/ui/graph.py index 00dfa8d546..73aed58ca3 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -411,7 +411,7 @@ def cancelNodeComputation(self, node): 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) @@ -524,9 +524,11 @@ def removeNodesFrom(self, startNode): 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)[0])] + for node in reversed(nodes): + self.removeNode(node) @Slot(Attribute, Attribute) def addEdge(self, src, dst): diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index a51c8ac5e2..d7db4cfb9c 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 @@ -152,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 } } @@ -168,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 @@ -219,15 +223,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 +255,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 @@ -283,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/Edge.qml b/meshroom/ui/qml/GraphEditor/Edge.qml index 730ab809da..e28a3f1818 100644 --- a/meshroom/ui/qml/GraphEditor/Edge.qml +++ b/meshroom/ui/qml/GraphEditor/Edge.qml @@ -41,8 +41,12 @@ Shape { startY: root.startY fillColor: "transparent" strokeColor: "#3E3E3E" - capStyle: ShapePath.RoundCap + strokeStyle: edge != undefined && ((edge.src != undefined && edge.src.isOutput) || edge.dst == undefined) ? ShapePath.SolidLine : ShapePath.DashLine 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 diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 8acbc718c3..9b6c2e11d9 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 fb28165966..2825dbbce7 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,283 @@ 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 - - // CompatibilityBadge icon for CompatibilityNodes - Loader { - active: root.isCompatibilityNode - sourceComponent: CompatibilityBadge { - sourceComponent: iconDelegate - canUpgrade: root.node.canUpgrade - issueDetails: root.node.issueDetails - } + 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 } - // 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() - - // 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 : "" + // 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 + } } - } - // 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" - } + // 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() + + // 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" + } - // 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 - - Rectangle { - anchors.fill: parent - color: Colors.sysPalette.mid - z: -1 + // 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 + } } - } - - // 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 + // Vertical Spacer + Item { width: parent.width; height: 2 } - enabled: !root.readOnly && !root.isCompatibilityNode + // Input/Output Attributes + Item { + id: nodeAttributes + width: parent.width - 2 + height: childrenRect.height + anchors.horizontalCenter: parent.horizontalCenter - Column { - width: parent.width - spacing: 5 - bottomPadding: 2 + enabled: !root.isCompatibilityNode Column { - id: outputs + id: attributesColumn width: parent.width - spacing: 3 - Repeater { - model: node ? node.attributes : undefined - - delegate: Loader { - id: outputLoader - active: object.isOutput && isDisplayableAsPin(object) - anchors.right: parent.right - width: outputs.width - - sourceComponent: AttributePin { - id: outPin - nodeItem: root - attribute: object - - readOnly: root.readOnly - onPressed: root.pressed(mouse) - Component.onCompleted: attributePinCreated(object, outPin) - Component.onDestruction: attributePinDeleted(attribute, outPin) + spacing: 5 + bottomPadding: 2 + + Column { + id: outputs + width: parent.width + spacing: 3 + Repeater { + model: node ? node.attributes : undefined + + delegate: Loader { + id: outputLoader + active: object.isOutput && isFileAttributeBaseType(object) + anchors.right: parent.right + width: outputs.width + + 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 + + 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 - - 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) + 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 + + 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 + } + } } } } diff --git a/meshroom/ui/qml/Viewer/ImageMetadataView.qml b/meshroom/ui/qml/Viewer/ImageMetadataView.qml index d35ef2d0af..7c86767f6a 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 diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 79f731b357..480deaf00e 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; @@ -240,8 +242,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