Merge pull request #1182 from alicevision/dev/graphDeps

Graph Dependencies
This commit is contained in:
Fabien Castan 2021-01-04 14:30:44 +01:00 committed by GitHub
commit c502ee1e73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 448 additions and 254 deletions

View file

@ -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
@ -137,7 +140,7 @@ class Attribute(BaseObject):
self.valueChanged.emit()
def resetValue(self):
self._value = ""
self._value = self.attributeDesc.value
def requestGraphUpdate(self):
if self.node.graph:
@ -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):

View file

@ -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):

View file

@ -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
@ -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

View file

@ -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")

View file

@ -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

View file

@ -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',

View file

@ -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'

View file

@ -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

View file

@ -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)
@ -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)[0])]
for node in reversed(nodes):
self.removeNode(node)
@Slot(Attribute, Attribute)
def addEdge(self, src, dst):

View file

@ -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
}
}

View file

@ -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

View file

@ -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

View file

@ -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
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: "<b>Shares internal folder (data) with other node(s). Hold click for details.</b>"
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: "<b>Shares internal folder (data) with other node(s). Hold click for details.</b>"
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.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
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
}
}
}
}
}

View file

@ -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

View file

@ -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