mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-05-19 20:16:30 +02:00
Reconnect copied/pasted nodes together but not to the graph
If several nodes are described in the clipboard and are supposed to be connected with each other, reconnect them together, but do not attempt to reconnect them to the graph they were copied from, even if it is the current graph.
This commit is contained in:
parent
a33f79e4d7
commit
d06acf6722
4 changed files with 161 additions and 61 deletions
|
@ -314,9 +314,8 @@ class Graph(BaseObject):
|
||||||
Returns:
|
Returns:
|
||||||
updatedData (dict): the dictionary containing all the nodes to import with their updated names and data
|
updatedData (dict): the dictionary containing all the nodes to import with their updated names and data
|
||||||
"""
|
"""
|
||||||
|
nameCorrespondences = {} # maps the old node name to its updated one
|
||||||
nameCorrespondences = {}
|
updatedData = {} # input data with updated node names and links
|
||||||
updatedData = {}
|
|
||||||
|
|
||||||
def createUniqueNodeName(nodeNames, inputName):
|
def createUniqueNodeName(nodeNames, inputName):
|
||||||
"""
|
"""
|
||||||
|
@ -330,26 +329,6 @@ class Graph(BaseObject):
|
||||||
return newName
|
return newName
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
def updateLinks(attributes):
|
|
||||||
"""
|
|
||||||
Update all the links that refer to nodes that are going to be imported and whose
|
|
||||||
name has to be updated.
|
|
||||||
"""
|
|
||||||
for key, val in attributes.items():
|
|
||||||
for corr in nameCorrespondences.keys():
|
|
||||||
if isinstance(val, pyCompatibility.basestring) and corr in val:
|
|
||||||
attributes[key] = val.replace(corr, nameCorrespondences[corr])
|
|
||||||
elif isinstance(val, list):
|
|
||||||
for v in val:
|
|
||||||
if isinstance(v, pyCompatibility.basestring):
|
|
||||||
if corr in v:
|
|
||||||
val[val.index(v)] = v.replace(corr, nameCorrespondences[corr])
|
|
||||||
else: # the list does not contain strings, so there cannot be links to update
|
|
||||||
break
|
|
||||||
attributes[key] = val
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
# First pass to get all the names that already exist in the graph, update them, and keep track of the changes
|
# First pass to get all the names that already exist in the graph, update them, and keep track of the changes
|
||||||
for nodeName, nodeData in sorted(data.items(), key=lambda x: self.getNodeIndexFromName(x[0])):
|
for nodeName, nodeData in sorted(data.items(), key=lambda x: self.getNodeIndexFromName(x[0])):
|
||||||
if not isinstance(nodeData, dict):
|
if not isinstance(nodeData, dict):
|
||||||
|
@ -363,20 +342,97 @@ class Graph(BaseObject):
|
||||||
else:
|
else:
|
||||||
updatedData[nodeName] = nodeData
|
updatedData[nodeName] = nodeData
|
||||||
|
|
||||||
|
newNames = [nodeName for nodeName in updatedData] # names of all the nodes that will be added
|
||||||
|
|
||||||
# Second pass to update all the links in the input/output attributes for every node with the new names
|
# Second pass to update all the links in the input/output attributes for every node with the new names
|
||||||
for nodeName, nodeData in updatedData.items():
|
for nodeName, nodeData in updatedData.items():
|
||||||
|
nodeType = nodeData.get("nodeType", None)
|
||||||
|
nodeDesc = meshroom.core.nodesDesc[nodeType]
|
||||||
|
|
||||||
inputs = nodeData.get("inputs", {})
|
inputs = nodeData.get("inputs", {})
|
||||||
outputs = nodeData.get("outputs", {})
|
outputs = nodeData.get("outputs", {})
|
||||||
|
|
||||||
if inputs:
|
if inputs:
|
||||||
inputs = updateLinks(inputs)
|
inputs = self.updateLinks(inputs, nameCorrespondences)
|
||||||
|
inputs = self.resetExternalLinks(inputs, nodeDesc.inputs, newNames)
|
||||||
updatedData[nodeName]["inputs"] = inputs
|
updatedData[nodeName]["inputs"] = inputs
|
||||||
if outputs:
|
if outputs:
|
||||||
outputs = updateLinks(outputs)
|
outputs = self.updateLinks(outputs, nameCorrespondences)
|
||||||
|
outputs = self.resetExternalLinks(outputs, nodeDesc.outputs, newNames)
|
||||||
updatedData[nodeName]["outputs"] = outputs
|
updatedData[nodeName]["outputs"] = outputs
|
||||||
|
|
||||||
return updatedData
|
return updatedData
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def updateLinks(attributes, nameCorrespondences):
|
||||||
|
"""
|
||||||
|
Update all the links that refer to nodes that are going to be imported and whose
|
||||||
|
names have to be updated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attributes (dict): attributes whose links need to be updated
|
||||||
|
nameCorrespondences (dict): node names to replace in the links with the name to replace them with
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
attributes (dict): the attributes with all the updated links
|
||||||
|
"""
|
||||||
|
for key, val in attributes.items():
|
||||||
|
for corr in nameCorrespondences.keys():
|
||||||
|
if isinstance(val, pyCompatibility.basestring) and corr in val:
|
||||||
|
attributes[key] = val.replace(corr, nameCorrespondences[corr])
|
||||||
|
elif isinstance(val, list):
|
||||||
|
for v in val:
|
||||||
|
if isinstance(v, pyCompatibility.basestring):
|
||||||
|
if corr in v:
|
||||||
|
val[val.index(v)] = v.replace(corr, nameCorrespondences[corr])
|
||||||
|
else: # the list does not contain strings, so there cannot be links to update
|
||||||
|
break
|
||||||
|
attributes[key] = val
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resetExternalLinks(attributes, nodeDesc, newNames):
|
||||||
|
"""
|
||||||
|
Reset all links to nodes that are not part of the nodes which are going to be imported:
|
||||||
|
if there are links to nodes that are not in the list, then it means that the references
|
||||||
|
are made to external nodes, and we want to get rid of those.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attributes (dict): attributes whose links might need to be reset
|
||||||
|
nodeDesc (list): list with all the attributes' description (including their default value)
|
||||||
|
newNames (list): names of the nodes that are going to be imported; no node name should be referenced
|
||||||
|
in the links except those contained in this list
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
attributes (dict): the attributes with all the links referencing nodes outside those which will be imported
|
||||||
|
reset to their default values
|
||||||
|
"""
|
||||||
|
for key, val in attributes.items():
|
||||||
|
defaultValue = None
|
||||||
|
for desc in nodeDesc:
|
||||||
|
if desc.name == key:
|
||||||
|
defaultValue = desc.value
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(val, pyCompatibility.basestring):
|
||||||
|
if Attribute.isLinkExpression(val) and not any(name in val for name in newNames):
|
||||||
|
if defaultValue is not None: # prevents from not entering condition if defaultValue = ''
|
||||||
|
attributes[key] = defaultValue
|
||||||
|
|
||||||
|
elif isinstance(val, list):
|
||||||
|
removedCnt = len(val) # counter to know whether all the list entries will be deemed invalid
|
||||||
|
tmpVal = list(val) # deep copy to ensure we iterate over the entire list (even if elements are removed)
|
||||||
|
for v in tmpVal:
|
||||||
|
if isinstance(v, pyCompatibility.basestring) and Attribute.isLinkExpression(v) and not any(
|
||||||
|
name in v for name in newNames):
|
||||||
|
val.remove(v)
|
||||||
|
removedCnt -= 1
|
||||||
|
if removedCnt == 0 and defaultValue is not None: # if all links were wrong, reset the attribute
|
||||||
|
attributes[key] = defaultValue
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def updateEnabled(self):
|
def updateEnabled(self):
|
||||||
return self._updateEnabled
|
return self._updateEnabled
|
||||||
|
@ -485,14 +541,39 @@ class Graph(BaseObject):
|
||||||
|
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
def pasteNode(self, nodeType, position, **kwargs):
|
def pasteNodes(self, data, position):
|
||||||
name = self._createUniqueNodeName(nodeType)
|
"""
|
||||||
node = None
|
Paste node(s) in the graph with their connections. The connections can only be between
|
||||||
|
the pasted nodes and not with the rest of the graph.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (dict): the dictionary containing the information about the nodes to paste, with their names and
|
||||||
|
links already updated to be added to the graph
|
||||||
|
position (list): the list of positions for each node to paste
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: the list of Node objects that were pasted and added to the graph
|
||||||
|
"""
|
||||||
|
nodes = []
|
||||||
with GraphModification(self):
|
with GraphModification(self):
|
||||||
node = Node(nodeType, position=position, **kwargs)
|
positionCnt = 0 # always valid because we know the data is sorted the same way as the position list
|
||||||
self._addNode(node, name)
|
for key in sorted(data):
|
||||||
|
nodeType = data[key].get("nodeType", None)
|
||||||
|
if not nodeType: # this case should never occur, as the data should have been prefiltered first
|
||||||
|
pass
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
attributes.update(data[key].get("inputs", {}))
|
||||||
|
attributes.update(data[key].get("outputs", {}))
|
||||||
|
|
||||||
|
node = Node(nodeType, position=position[positionCnt], **attributes)
|
||||||
|
self._addNode(node, key)
|
||||||
|
|
||||||
|
nodes.append(node)
|
||||||
|
positionCnt += 1
|
||||||
|
|
||||||
self._applyExpr()
|
self._applyExpr()
|
||||||
return node
|
return nodes
|
||||||
|
|
||||||
def outEdges(self, attribute):
|
def outEdges(self, attribute):
|
||||||
""" Return the list of edges starting from the given attribute """
|
""" Return the list of edges starting from the given attribute """
|
||||||
|
|
|
@ -195,25 +195,26 @@ class DuplicateNodesCommand(GraphCommand):
|
||||||
self.graph.removeNode(duplicate)
|
self.graph.removeNode(duplicate)
|
||||||
|
|
||||||
|
|
||||||
class PasteNodeCommand(GraphCommand):
|
class PasteNodesCommand(GraphCommand):
|
||||||
"""
|
"""
|
||||||
Handle node pasting in a Graph.
|
Handle node pasting in a Graph.
|
||||||
"""
|
"""
|
||||||
def __init__(self, graph, nodeType, position=None, parent=None, **kwargs):
|
def __init__(self, graph, data, position=None, parent=None):
|
||||||
super(PasteNodeCommand, self).__init__(graph, parent)
|
super(PasteNodesCommand, self).__init__(graph, parent)
|
||||||
self.nodeType = nodeType
|
self.data = data
|
||||||
self.position = position
|
self.position = position
|
||||||
self.nodeName = None
|
self.nodeNames = []
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
def redoImpl(self):
|
def redoImpl(self):
|
||||||
node = self.graph.pasteNode(self.nodeType, self.position, **self.kwargs)
|
data = self.graph.updateImportedScene(self.data)
|
||||||
self.nodeName = node.name
|
nodes = self.graph.pasteNodes(data, self.position)
|
||||||
self.setText("Paste Node {}".format(self.nodeName))
|
self.nodeNames = [node.name for node in nodes]
|
||||||
return node
|
self.setText("Paste Node{} ({})".format("s" if len(self.nodeNames) > 1 else "", ", ".join(self.nodeNames)))
|
||||||
|
return nodes
|
||||||
|
|
||||||
def undoImpl(self):
|
def undoImpl(self):
|
||||||
self.graph.removeNode(self.nodeName)
|
for name in self.nodeNames:
|
||||||
|
self.graph.removeNode(name)
|
||||||
|
|
||||||
|
|
||||||
class ImportSceneCommand(GraphCommand):
|
class ImportSceneCommand(GraphCommand):
|
||||||
|
@ -247,6 +248,7 @@ class ImportSceneCommand(GraphCommand):
|
||||||
self.graph.removeNode(nodeName)
|
self.graph.removeNode(nodeName)
|
||||||
self.importedNames = []
|
self.importedNames = []
|
||||||
|
|
||||||
|
|
||||||
class SetAttributeCommand(GraphCommand):
|
class SetAttributeCommand(GraphCommand):
|
||||||
def __init__(self, graph, attribute, value, parent=None):
|
def __init__(self, graph, attribute, value, parent=None):
|
||||||
super(SetAttributeCommand, self).__init__(graph, parent)
|
super(SetAttributeCommand, self).__init__(graph, parent)
|
||||||
|
|
|
@ -781,7 +781,7 @@ class UIGraph(QObject):
|
||||||
return json.dumps(selection, indent=4)
|
return json.dumps(selection, indent=4)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@Slot(str, QPoint)
|
@Slot(str, QPoint, result="QVariantList")
|
||||||
def pasteNodes(self, clipboardContent, position=None):
|
def pasteNodes(self, clipboardContent, position=None):
|
||||||
"""
|
"""
|
||||||
Parse the content of the clipboard to see whether it contains
|
Parse the content of the clipboard to see whether it contains
|
||||||
|
@ -789,18 +789,26 @@ class UIGraph(QObject):
|
||||||
in the clipboard are built with the available information.
|
in the clipboard are built with the available information.
|
||||||
Otherwise, nothing is done.
|
Otherwise, nothing is done.
|
||||||
|
|
||||||
This function does not need to be preceded by a call to "copyNodeContent".
|
This function does not need to be preceded by a call to "getSelectedNodesContent".
|
||||||
Any clipboard content that contains at least a node type with a valid JSON
|
Any clipboard content that contains at least a node type with a valid JSON
|
||||||
formatting (dictionary form with double quotes around the keys and values)
|
formatting (dictionary form with double quotes around the keys and values)
|
||||||
can be used to generate a node.
|
can be used to generate a node.
|
||||||
|
|
||||||
For example, it is enough to have:
|
For example, it is enough to have:
|
||||||
{"nodeName": {"nodeType":"CameraInit"}}
|
{"nodeName_1": {"nodeType":"CameraInit"}, "nodeName_2": {"nodeType":"FeatureMatching"}}
|
||||||
in the clipboard to create a default CameraInit node.
|
in the clipboard to create a default CameraInit and a default FeatureMatching nodes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
clipboardContent (str): the string contained in the clipboard, that may or may not contain valid
|
||||||
|
node information
|
||||||
|
position (QPoint): the position of the mouse in the Graph Editor when the function was called
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: the list of Node objects that were pasted and added to the graph
|
||||||
"""
|
"""
|
||||||
if not clipboardContent:
|
if not clipboardContent:
|
||||||
return
|
return
|
||||||
d = {}
|
|
||||||
try:
|
try:
|
||||||
d = json.loads(clipboardContent)
|
d = json.loads(clipboardContent)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
@ -809,35 +817,39 @@ class UIGraph(QObject):
|
||||||
if not isinstance(d, dict):
|
if not isinstance(d, dict):
|
||||||
raise ValueError("The clipboard does not contain a valid node. Cannot paste it.")
|
raise ValueError("The clipboard does not contain a valid node. Cannot paste it.")
|
||||||
|
|
||||||
|
# If the clipboard contains a header, then a whole file is contained in the clipboard
|
||||||
|
# Extract the "graph" part and paste it all, ignore the rest
|
||||||
|
if d.get("header", None):
|
||||||
|
d = d.get("graph", None)
|
||||||
|
if not d:
|
||||||
|
return
|
||||||
|
|
||||||
finalPosition = None
|
finalPosition = None
|
||||||
prevPosition = None
|
prevPosition = None
|
||||||
|
positions = []
|
||||||
|
|
||||||
for key in d:
|
for key in sorted(d):
|
||||||
nodeDesc = d[key]
|
nodeType = d[key].get("nodeType", None)
|
||||||
nodeType = nodeDesc.get("nodeType", None)
|
|
||||||
if not nodeType:
|
if not nodeType:
|
||||||
pass
|
raise ValueError("Invalid node description: no provided node type for '{}'".format(key))
|
||||||
|
|
||||||
attributes = {}
|
currentPosition = d[key].get("position", None)
|
||||||
attributes.update(nodeDesc.get("inputs", {}))
|
|
||||||
attributes.update(nodeDesc.get("outputs", {}))
|
|
||||||
|
|
||||||
currentPosition = nodeDesc.get("position", None)
|
|
||||||
if isinstance(position, QPoint) and not finalPosition:
|
if isinstance(position, QPoint) and not finalPosition:
|
||||||
finalPosition = Position(position.x(), position.y())
|
finalPosition = Position(position.x(), position.y())
|
||||||
else:
|
else:
|
||||||
if prevPosition and currentPosition:
|
if prevPosition and currentPosition:
|
||||||
# if the nodes both have a position, recreate the distance between them from a different
|
# If the nodes both have a position, recreate the distance between them with a different
|
||||||
# starting point
|
# starting point
|
||||||
x = finalPosition.x + (currentPosition[0] - prevPosition[0])
|
x = finalPosition.x + (currentPosition[0] - prevPosition[0])
|
||||||
y = finalPosition.y + (currentPosition[1] - prevPosition[1])
|
y = finalPosition.y + (currentPosition[1] - prevPosition[1])
|
||||||
finalPosition = Position(x, y)
|
finalPosition = Position(x, y)
|
||||||
else:
|
else:
|
||||||
# if either the current node or previous one lack a position, use a custom one
|
# If either the current node or previous one lacks a position, use a custom one
|
||||||
finalPosition = Position(finalPosition.x + self.layout.gridSpacing + self.layout.nodeWidth, finalPosition.y)
|
finalPosition = Position(finalPosition.x + self.layout.gridSpacing + self.layout.nodeWidth, finalPosition.y)
|
||||||
|
|
||||||
self.push(commands.PasteNodeCommand(self._graph, nodeType, position=finalPosition, **attributes))
|
|
||||||
prevPosition = currentPosition
|
prevPosition = currentPosition
|
||||||
|
positions.append(finalPosition)
|
||||||
|
|
||||||
|
return self.push(commands.PasteNodesCommand(self.graph, d, position=positions))
|
||||||
|
|
||||||
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
|
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
|
||||||
graphChanged = Signal()
|
graphChanged = Signal()
|
||||||
|
|
|
@ -91,7 +91,12 @@ Item {
|
||||||
{
|
{
|
||||||
root.pastePosition = mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY)
|
root.pastePosition = mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY)
|
||||||
var copiedContent = Clipboard.getText()
|
var copiedContent = Clipboard.getText()
|
||||||
uigraph.pasteNodes(copiedContent, root.pastePosition)
|
var nodes = uigraph.pasteNodes(copiedContent, root.pastePosition)
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
uigraph.clearNodeSelection()
|
||||||
|
uigraph.selectedNode = nodes[0]
|
||||||
|
uigraph.selectNodes(nodes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: {
|
Keys.onPressed: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue