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