Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/alicevision/meshroom.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCandice Bentéjac <candice.bentejac@gmail.com>2022-09-01 18:32:18 +0300
committerCandice Bentéjac <candice.bentejac@gmail.com>2022-09-06 10:43:19 +0300
commitc44b2a8c006462113825f007b38eba3651beaa58 (patch)
tree259306445f60f8c1a975800c4a18961e9e70282a
parent1f800aefd5a79dece6219ee7412076bc501b28cb (diff)
Add "minimal" mode for template files
Add a specific option to save a graph as a template ("Save As Template") in "minimal" mode. This mode only saves, for each node in the graph, the input and output attributes whose values differ from the default ones. Any attribute that is not serialized in the saved template file is assumed to have its default value. If a conflict is detected on a node when opening the template file, it is logged but solved automatically. The goal of this minimal mode is to prevent template files from needing an update every single time a modification (may it be minor or major) is done on a node description. Templates can thus follow node changes and still be usable.
-rw-r--r--meshroom/core/graph.py53
-rw-r--r--meshroom/core/node.py11
-rw-r--r--meshroom/ui/graph.py10
-rwxr-xr-xmeshroom/ui/qml/main.qml27
4 files changed, 90 insertions, 11 deletions
diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py
index 89faefb5..817af336 100644
--- a/meshroom/core/graph.py
+++ b/meshroom/core/graph.py
@@ -261,6 +261,9 @@ class Graph(BaseObject):
self.header = fileData.get(Graph.IO.Keys.Header, {})
nodesVersions = self.header.get(Graph.IO.Keys.NodesVersions, {})
+ # check whether the file was saved as a template in minimal mode
+ isTemplate = self.header.get("template", False)
+
with GraphModification(self):
# iterate over nodes sorted by suffix index in their names
for nodeName, nodeData in sorted(graphData.items(), key=lambda x: self.getNodeIndexFromName(x[0])):
@@ -273,7 +276,8 @@ class Graph(BaseObject):
# 3. fallback to no version "0.0": retro-compatibility
if "version" not in nodeData:
nodeData["version"] = nodesVersions.get(nodeData["nodeType"], "0.0")
- n = nodeFactory(nodeData, nodeName)
+
+ n = nodeFactory(nodeData, nodeName, template=isTemplate)
# Add node to the graph with raw attributes values
self._addNode(n, nodeName)
@@ -999,7 +1003,7 @@ class Graph(BaseObject):
def asString(self):
return str(self.toDict())
- def save(self, filepath=None, setupProjectFile=True):
+ def save(self, filepath=None, setupProjectFile=True, template=False):
path = filepath or self._filepath
if not path:
raise ValueError("filepath must be specified for unsaved files.")
@@ -1015,10 +1019,18 @@ class Graph(BaseObject):
for p in usedNodeTypes
}
- data = {
- Graph.IO.Keys.Header: self.header,
- Graph.IO.Keys.Graph: self.toDict()
- }
+ data = {}
+ if template:
+ self.header["template"] = True
+ data = {
+ Graph.IO.Keys.Header: self.header,
+ Graph.IO.Keys.Graph: self.getNonDefaultAttributes()
+ }
+ else:
+ data = {
+ Graph.IO.Keys.Header: self.header,
+ Graph.IO.Keys.Graph: self.toDict()
+ }
with open(path, 'w') as jsonFile:
json.dump(data, jsonFile, indent=4)
@@ -1026,6 +1038,35 @@ class Graph(BaseObject):
if path != self._filepath and setupProjectFile:
self._setFilepath(path)
+ def getNonDefaultAttributes(self):
+ """
+ Instead of getting all the inputs/outputs attribute keys, only get the keys of
+ the attributes whose value is not the default one.
+
+ Returns:
+ dict: self.toDict() with all the inputs/outputs attributes with default values removed
+ """
+ graph = self.toDict()
+ for nodeName in graph.keys():
+ node = self.node(nodeName)
+
+ inputKeys = list(graph[nodeName]["inputs"].keys())
+ outputKeys = list(graph[nodeName]["outputs"].keys())
+
+ for attrName in inputKeys:
+ attribute = node.attribute(attrName)
+ # check that attribute is not a link for choice attributes
+ if attribute.isDefault and not attribute.isLink:
+ del graph[nodeName]["inputs"][attrName]
+
+ for attrName in outputKeys:
+ attribute = node.attribute(attrName)
+ # check that attribute is not a link for choice attributes
+ if attribute.isDefault and not attribute.isLink:
+ del graph[nodeName]["outputs"][attrName]
+
+ return graph
+
def _setFilepath(self, filepath):
"""
Set the internal filepath of this Graph.
diff --git a/meshroom/core/node.py b/meshroom/core/node.py
index 2149c8cd..ecaab7fc 100644
--- a/meshroom/core/node.py
+++ b/meshroom/core/node.py
@@ -1393,7 +1393,7 @@ class CompatibilityNode(BaseNode):
issueDetails = Property(str, issueDetails.fget, constant=True)
-def nodeFactory(nodeDict, name=None):
+def nodeFactory(nodeDict, name=None, template=False):
"""
Create a node instance by deserializing the given node data.
If the serialized data matches the corresponding node type description, a Node instance is created.
@@ -1437,9 +1437,10 @@ def nodeFactory(nodeDict, name=None):
compatibilityIssue = CompatibilityIssue.VersionConflict
# in other cases, check attributes compatibility between serialized node and its description
else:
- # check that the node has the exact same set of inputs/outputs as its description
- if sorted([attr.name for attr in nodeDesc.inputs]) != sorted(inputs.keys()) or \
- sorted([attr.name for attr in nodeDesc.outputs]) != sorted(outputs.keys()):
+ # check that the node has the exact same set of inputs/outputs as its description, except if the node
+ # is described in a template file, in which only non-default parameters are saved
+ if not template and (sorted([attr.name for attr in nodeDesc.inputs]) != sorted(inputs.keys()) or \
+ sorted([attr.name for attr in nodeDesc.outputs]) != sorted(outputs.keys())):
compatibilityIssue = CompatibilityIssue.DescriptionConflict
# verify that all inputs match their descriptions
for attrName, value in inputs.items():
@@ -1463,5 +1464,7 @@ def nodeFactory(nodeDict, name=None):
if not internalFolder and nodeDesc:
logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name))
node = node.upgrade()
+ elif template: # if the node comes from a template file and there is a conflict, it should be upgraded anyway
+ node = node.upgrade()
return node
diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py
index 71846a9f..0b450562 100644
--- a/meshroom/ui/graph.py
+++ b/meshroom/ui/graph.py
@@ -354,6 +354,14 @@ class UIGraph(QObject):
@Slot(QUrl)
def saveAs(self, url):
+ self._saveAs(url)
+
+ @Slot(QUrl)
+ def saveAsTemplate(self, url):
+ self._saveAs(url, setupProjectFile=False, template=True)
+
+ def _saveAs(self, url, setupProjectFile=True, template=False):
+ """ Helper function for 'save as' features. """
if isinstance(url, (str)):
localFile = url
else:
@@ -361,7 +369,7 @@ class UIGraph(QObject):
# ensure file is saved with ".mg" extension
if os.path.splitext(localFile)[-1] != ".mg":
localFile += ".mg"
- self._graph.save(localFile)
+ self._graph.save(localFile, setupProjectFile=setupProjectFile, template=template)
self._undoStack.setClean()
# saving file on disk impacts cache folder location
# => force re-evaluation of monitored status files paths
diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml
index 5b4197d6..63f056f4 100755
--- a/meshroom/ui/qml/main.qml
+++ b/meshroom/ui/qml/main.qml
@@ -142,6 +142,22 @@ ApplicationWindow {
onRejected: closed(Platform.Dialog.Rejected)
}
+ Platform.FileDialog {
+ id: saveTemplateDialog
+
+ signal closed(var result)
+
+ title: "Save Template"
+ nameFilters: ["Meshroom Graphs (*.mg)"]
+ defaultSuffix: ".mg"
+ fileMode: Platform.FileDialog.SaveFile
+ onAccepted: {
+ _reconstruction.saveAsTemplate(file)
+ closed(Platform.Dialog.Accepted)
+ }
+ onRejected: closed(Platform.Dialog.Rejected)
+ }
+
Item {
id: computeManager
@@ -550,6 +566,17 @@ ApplicationWindow {
saveFileDialog.open()
}
}
+ Action {
+ id: saveAsTemplateAction
+ text: "Save As Template..."
+ shortcut: "Ctrl+Shift+T"
+ onTriggered: {
+ if(_reconstruction.graph && _reconstruction.graph.filepath) {
+ saveTemplateDialog.folder = Filepath.stringToUrl(Filepath.dirname(_reconstruction.graph.filepath))
+ }
+ saveTemplateDialog.open()
+ }
+ }
MenuSeparator { }
Action {
text: "Quit"