mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-05-19 12:06:28 +02:00
Merge pull request #1758 from alicevision/dev/copyPasteNode
Support copying/pasting a node in the Graph Editor
This commit is contained in:
commit
27faf9f77c
7 changed files with 498 additions and 7 deletions
|
@ -219,6 +219,7 @@ class Graph(BaseObject):
|
|||
self._canComputeLeaves = True
|
||||
self._nodes = DictModel(keyAttrName='name', parent=self)
|
||||
self._edges = DictModel(keyAttrName='dst', parent=self) # use dst attribute as unique key since it can only have one input connection
|
||||
self._importedNodes = DictModel(keyAttrName='name', parent=self)
|
||||
self._compatibilityNodes = DictModel(keyAttrName='name', parent=self)
|
||||
self.cacheDir = meshroom.core.defaultCacheFolder
|
||||
self._filepath = ''
|
||||
|
@ -231,6 +232,7 @@ class Graph(BaseObject):
|
|||
# Tell QML nodes are going to be deleted
|
||||
for node in self._nodes:
|
||||
node.alive = False
|
||||
self._importedNodes.clear()
|
||||
self._nodes.clear()
|
||||
|
||||
@property
|
||||
|
@ -239,7 +241,7 @@ class Graph(BaseObject):
|
|||
return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0"))
|
||||
|
||||
@Slot(str)
|
||||
def load(self, filepath, setupProjectFile=True):
|
||||
def load(self, filepath, setupProjectFile=True, importScene=False):
|
||||
"""
|
||||
Load a meshroom graph ".mg" file.
|
||||
|
||||
|
@ -248,13 +250,18 @@ class Graph(BaseObject):
|
|||
setupProjectFile: Store the reference to the project file and setup the cache directory.
|
||||
If false, it only loads the graph of the project file as a template.
|
||||
"""
|
||||
self.clear()
|
||||
if not importScene:
|
||||
self.clear()
|
||||
with open(filepath) as jsonFile:
|
||||
fileData = json.load(jsonFile)
|
||||
|
||||
# older versions of Meshroom files only contained the serialized nodes
|
||||
graphData = fileData.get(Graph.IO.Keys.Graph, fileData)
|
||||
|
||||
if importScene:
|
||||
self._importedNodes.clear()
|
||||
graphData = self.updateImportedScene(graphData)
|
||||
|
||||
if not isinstance(graphData, dict):
|
||||
raise RuntimeError('loadGraph error: Graph is not a dict. File: {}'.format(filepath))
|
||||
|
||||
|
@ -282,6 +289,9 @@ class Graph(BaseObject):
|
|||
# Add node to the graph with raw attributes values
|
||||
self._addNode(n, nodeName)
|
||||
|
||||
if importScene:
|
||||
self._importedNodes.add(n)
|
||||
|
||||
# Create graph edges by resolving attributes expressions
|
||||
self._applyExpr()
|
||||
|
||||
|
@ -292,6 +302,141 @@ class Graph(BaseObject):
|
|||
|
||||
return True
|
||||
|
||||
def updateImportedScene(self, data):
|
||||
"""
|
||||
Update the names and links of the scene to import so that it can fit
|
||||
correctly in the existing graph.
|
||||
|
||||
Parse all the nodes from the scene that is going to be imported.
|
||||
If their name already exists in the graph, replace them with new names,
|
||||
then parse all the nodes' inputs/outputs to replace the old names with
|
||||
the new ones in the links.
|
||||
|
||||
Args:
|
||||
data (dict): the dictionary containing all the nodes to import and their data
|
||||
|
||||
Returns:
|
||||
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
|
||||
updatedData = {} # input data with updated node names and links
|
||||
|
||||
def createUniqueNodeName(nodeNames, inputName):
|
||||
"""
|
||||
Create a unique name that does not already exist in the current graph or in the list
|
||||
of nodes that will be imported.
|
||||
"""
|
||||
i = 1
|
||||
while i:
|
||||
newName = "{name}_{index}".format(name=inputName, index=i)
|
||||
if newName not in nodeNames and newName not in updatedData.keys():
|
||||
return newName
|
||||
i += 1
|
||||
|
||||
# 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])):
|
||||
if not isinstance(nodeData, dict):
|
||||
raise RuntimeError('updateImportedScene error: Node is not a dict.')
|
||||
|
||||
if nodeName in self._nodes.keys() or nodeName in updatedData.keys():
|
||||
newName = createUniqueNodeName(self._nodes.keys(), nodeData["nodeType"])
|
||||
updatedData[newName] = nodeData
|
||||
nameCorrespondences[nodeName] = newName
|
||||
|
||||
else:
|
||||
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
|
||||
for nodeName, nodeData in updatedData.items():
|
||||
nodeType = nodeData.get("nodeType", None)
|
||||
nodeDesc = meshroom.core.nodesDesc[nodeType]
|
||||
|
||||
inputs = nodeData.get("inputs", {})
|
||||
outputs = nodeData.get("outputs", {})
|
||||
|
||||
if inputs:
|
||||
inputs = self.updateLinks(inputs, nameCorrespondences)
|
||||
inputs = self.resetExternalLinks(inputs, nodeDesc.inputs, newNames)
|
||||
updatedData[nodeName]["inputs"] = inputs
|
||||
if outputs:
|
||||
outputs = self.updateLinks(outputs, nameCorrespondences)
|
||||
outputs = self.resetExternalLinks(outputs, nodeDesc.outputs, newNames)
|
||||
updatedData[nodeName]["outputs"] = outputs
|
||||
|
||||
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
|
||||
def updateEnabled(self):
|
||||
return self._updateEnabled
|
||||
|
@ -400,6 +545,40 @@ class Graph(BaseObject):
|
|||
|
||||
return duplicates
|
||||
|
||||
def pasteNodes(self, data, position):
|
||||
"""
|
||||
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):
|
||||
positionCnt = 0 # always valid because we know the data is sorted the same way as the position list
|
||||
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()
|
||||
return nodes
|
||||
|
||||
def outEdges(self, attribute):
|
||||
""" Return the list of edges starting from the given attribute """
|
||||
# type: (Attribute,) -> [Edge]
|
||||
|
@ -436,6 +615,8 @@ class Graph(BaseObject):
|
|||
|
||||
node.alive = False
|
||||
self._nodes.remove(node)
|
||||
if node in self._importedNodes:
|
||||
self._importedNodes.remove(node)
|
||||
self.update()
|
||||
|
||||
return inEdges, outEdges
|
||||
|
@ -1220,6 +1401,11 @@ class Graph(BaseObject):
|
|||
def edges(self):
|
||||
return self._edges
|
||||
|
||||
@property
|
||||
def importedNodes(self):
|
||||
"""" Return the list of nodes that were added to the graph with the latest 'Import Scene' action. """
|
||||
return self._importedNodes
|
||||
|
||||
@property
|
||||
def cacheDir(self):
|
||||
return self._cacheDir
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue