mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 18:27:23 +02:00
Import a scene from an .mg file into the current graph
Add an "Import Scene" (Ctrl+Shift+I) option in the File menu that allows to select an existing .mg file and import it in the current graph. The imported scene will automatically be placed below the lowest existing node along the Y axis.
This commit is contained in:
parent
ede24713d0
commit
a33f79e4d7
4 changed files with 159 additions and 3 deletions
|
@ -219,6 +219,7 @@ class Graph(BaseObject):
|
||||||
self._canComputeLeaves = True
|
self._canComputeLeaves = True
|
||||||
self._nodes = DictModel(keyAttrName='name', parent=self)
|
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._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._compatibilityNodes = DictModel(keyAttrName='name', parent=self)
|
||||||
self.cacheDir = meshroom.core.defaultCacheFolder
|
self.cacheDir = meshroom.core.defaultCacheFolder
|
||||||
self._filepath = ''
|
self._filepath = ''
|
||||||
|
@ -231,6 +232,7 @@ class Graph(BaseObject):
|
||||||
# Tell QML nodes are going to be deleted
|
# Tell QML nodes are going to be deleted
|
||||||
for node in self._nodes:
|
for node in self._nodes:
|
||||||
node.alive = False
|
node.alive = False
|
||||||
|
self._importedNodes.clear()
|
||||||
self._nodes.clear()
|
self._nodes.clear()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -239,7 +241,7 @@ class Graph(BaseObject):
|
||||||
return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0"))
|
return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0"))
|
||||||
|
|
||||||
@Slot(str)
|
@Slot(str)
|
||||||
def load(self, filepath, setupProjectFile=True):
|
def load(self, filepath, setupProjectFile=True, importScene=False):
|
||||||
"""
|
"""
|
||||||
Load a meshroom graph ".mg" file.
|
Load a meshroom graph ".mg" file.
|
||||||
|
|
||||||
|
@ -248,6 +250,7 @@ class Graph(BaseObject):
|
||||||
setupProjectFile: Store the reference to the project file and setup the cache directory.
|
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.
|
If false, it only loads the graph of the project file as a template.
|
||||||
"""
|
"""
|
||||||
|
if not importScene:
|
||||||
self.clear()
|
self.clear()
|
||||||
with open(filepath) as jsonFile:
|
with open(filepath) as jsonFile:
|
||||||
fileData = json.load(jsonFile)
|
fileData = json.load(jsonFile)
|
||||||
|
@ -255,6 +258,10 @@ class Graph(BaseObject):
|
||||||
# older versions of Meshroom files only contained the serialized nodes
|
# older versions of Meshroom files only contained the serialized nodes
|
||||||
graphData = fileData.get(Graph.IO.Keys.Graph, fileData)
|
graphData = fileData.get(Graph.IO.Keys.Graph, fileData)
|
||||||
|
|
||||||
|
if importScene:
|
||||||
|
self._importedNodes.clear()
|
||||||
|
graphData = self.updateImportedScene(graphData)
|
||||||
|
|
||||||
if not isinstance(graphData, dict):
|
if not isinstance(graphData, dict):
|
||||||
raise RuntimeError('loadGraph error: Graph is not a dict. File: {}'.format(filepath))
|
raise RuntimeError('loadGraph error: Graph is not a dict. File: {}'.format(filepath))
|
||||||
|
|
||||||
|
@ -278,6 +285,9 @@ class Graph(BaseObject):
|
||||||
# Add node to the graph with raw attributes values
|
# Add node to the graph with raw attributes values
|
||||||
self._addNode(n, nodeName)
|
self._addNode(n, nodeName)
|
||||||
|
|
||||||
|
if importScene:
|
||||||
|
self._importedNodes.add(n)
|
||||||
|
|
||||||
# Create graph edges by resolving attributes expressions
|
# Create graph edges by resolving attributes expressions
|
||||||
self._applyExpr()
|
self._applyExpr()
|
||||||
|
|
||||||
|
@ -288,6 +298,85 @@ class Graph(BaseObject):
|
||||||
|
|
||||||
return True
|
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 = {}
|
||||||
|
updatedData = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
inputs = nodeData.get("inputs", {})
|
||||||
|
outputs = nodeData.get("outputs", {})
|
||||||
|
|
||||||
|
if inputs:
|
||||||
|
inputs = updateLinks(inputs)
|
||||||
|
updatedData[nodeName]["inputs"] = inputs
|
||||||
|
if outputs:
|
||||||
|
outputs = updateLinks(outputs)
|
||||||
|
updatedData[nodeName]["outputs"] = outputs
|
||||||
|
|
||||||
|
return updatedData
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def updateEnabled(self):
|
def updateEnabled(self):
|
||||||
return self._updateEnabled
|
return self._updateEnabled
|
||||||
|
@ -441,6 +530,8 @@ class Graph(BaseObject):
|
||||||
|
|
||||||
node.alive = False
|
node.alive = False
|
||||||
self._nodes.remove(node)
|
self._nodes.remove(node)
|
||||||
|
if node in self._importedNodes:
|
||||||
|
self._importedNodes.remove(node)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
return inEdges, outEdges
|
return inEdges, outEdges
|
||||||
|
@ -1188,6 +1279,11 @@ class Graph(BaseObject):
|
||||||
def edges(self):
|
def edges(self):
|
||||||
return self._edges
|
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
|
@property
|
||||||
def cacheDir(self):
|
def cacheDir(self):
|
||||||
return self._cacheDir
|
return self._cacheDir
|
||||||
|
|
|
@ -7,7 +7,7 @@ from PySide2.QtCore import Property, Signal
|
||||||
|
|
||||||
from meshroom.core.attribute import ListAttribute, Attribute
|
from meshroom.core.attribute import ListAttribute, Attribute
|
||||||
from meshroom.core.graph import GraphModification
|
from meshroom.core.graph import GraphModification
|
||||||
from meshroom.core.node import nodeFactory
|
from meshroom.core.node import nodeFactory, Position
|
||||||
|
|
||||||
|
|
||||||
class UndoCommand(QUndoCommand):
|
class UndoCommand(QUndoCommand):
|
||||||
|
@ -216,6 +216,37 @@ class PasteNodeCommand(GraphCommand):
|
||||||
self.graph.removeNode(self.nodeName)
|
self.graph.removeNode(self.nodeName)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportSceneCommand(GraphCommand):
|
||||||
|
"""
|
||||||
|
Handle the import of a scene into a Graph.
|
||||||
|
"""
|
||||||
|
def __init__(self, graph, filepath=None, yOffset=0, parent=None):
|
||||||
|
super(ImportSceneCommand, self).__init__(graph, parent)
|
||||||
|
self.filepath = filepath
|
||||||
|
self.importedNames = []
|
||||||
|
self.yOffset = yOffset
|
||||||
|
|
||||||
|
def redoImpl(self):
|
||||||
|
status = self.graph.load(self.filepath, setupProjectFile=False, importScene=True)
|
||||||
|
importedNodes = self.graph.importedNodes
|
||||||
|
self.setText("Import Scene ({} nodes)".format(importedNodes.count))
|
||||||
|
|
||||||
|
lowestY = 0
|
||||||
|
for node in self.graph.nodes:
|
||||||
|
if node not in importedNodes and node.y > lowestY:
|
||||||
|
lowestY = node.y
|
||||||
|
|
||||||
|
for node in importedNodes:
|
||||||
|
self.importedNames.append(node.name)
|
||||||
|
self.graph.node(node.name).position = Position(node.x, node.y + lowestY + self.yOffset)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def undoImpl(self):
|
||||||
|
for nodeName in self.importedNames:
|
||||||
|
self.graph.removeNode(nodeName)
|
||||||
|
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)
|
||||||
|
|
|
@ -353,6 +353,19 @@ class UIGraph(QObject):
|
||||||
self.setGraph(g)
|
self.setGraph(g)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
@Slot(QUrl, result=bool)
|
||||||
|
def importScene(self, filepath):
|
||||||
|
if isinstance(filepath, (QUrl)):
|
||||||
|
# depending how the QUrl has been initialized,
|
||||||
|
# toLocalFile() may return the local path or an empty string
|
||||||
|
localFile = filepath.toLocalFile()
|
||||||
|
if not localFile:
|
||||||
|
localFile = filepath.toString()
|
||||||
|
else:
|
||||||
|
localFile = filepath
|
||||||
|
yOffset = self.layout.gridSpacing + self.layout.nodeHeight
|
||||||
|
return self.push(commands.ImportSceneCommand(self._graph, localFile, yOffset=yOffset))
|
||||||
|
|
||||||
@Slot(QUrl)
|
@Slot(QUrl)
|
||||||
def saveAs(self, url):
|
def saveAs(self, url):
|
||||||
if isinstance(url, (str)):
|
if isinstance(url, (str)):
|
||||||
|
|
|
@ -304,6 +304,16 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: importSceneDialog
|
||||||
|
title: "Import Scene"
|
||||||
|
selectMultiple: false
|
||||||
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
||||||
|
onAccepted: {
|
||||||
|
graphEditor.uigraph.importScene(importSceneDialog.fileUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AboutDialog {
|
AboutDialog {
|
||||||
id: aboutDialog
|
id: aboutDialog
|
||||||
}
|
}
|
||||||
|
@ -530,6 +540,12 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action {
|
||||||
|
id: importSceneAction
|
||||||
|
text: "Import Scene"
|
||||||
|
shortcut: "Ctrl+Shift+I"
|
||||||
|
onTriggered: importSceneDialog.open()
|
||||||
|
}
|
||||||
Action {
|
Action {
|
||||||
id: importActionItem
|
id: importActionItem
|
||||||
text: "Import Images"
|
text: "Import Images"
|
||||||
|
|
Loading…
Add table
Reference in a new issue