diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index fe9ec170..82635202 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -31,7 +31,6 @@ logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=log sessionUid = str(uuid.uuid1()) cacheFolderName = 'MeshroomCache' -defaultCacheFolder = os.environ.get('MESHROOM_CACHE', os.path.join(tempfile.gettempdir(), cacheFolderName)) nodesDesc = {} submitters = {} pipelineTemplates = {} diff --git a/meshroom/core/desc/node.py b/meshroom/core/desc/node.py index 2cbec111..e524037e 100644 --- a/meshroom/core/desc/node.py +++ b/meshroom/core/desc/node.py @@ -25,18 +25,6 @@ class MrNodeType(enum.Enum): COMMANDLINE = enum.auto() INPUT = enum.auto() -def isNodeSaved(node): - """Returns whether a node is identical to its serialized counterpart in the current graph file.""" - filepath = node.graph.filepath - if not filepath: - return False - - from meshroom.core.graph import loadGraph - graphSaved = loadGraph(filepath) - nodeSaved = graphSaved.node(node.name) - if nodeSaved is None: - return False - return nodeSaved._uid == node._uid class BaseNode(object): """ @@ -259,9 +247,6 @@ class Node(BaseNode): return MrNodeType.NODE def processChunkInEnvironment(self, chunk): - if not isNodeSaved(chunk.node): - raise RuntimeError("File must be saved before computing in isolated environment.") - meshroomComputeCmd = f"python {_MESHROOM_COMPUTE} {chunk.node.graph.filepath} --node {chunk.node.name} --extern --inCurrentEnv" if len(chunk.node.getChunks()) > 1: meshroomComputeCmd += f" --iteration {chunk.range.iteration}" diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 11cb1965..ab28bcc3 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -182,7 +182,6 @@ class Graph(BaseObject): edges = {B.input: A.output, C.input: B.output,} """ - _cacheDir = "" def __init__(self, name, parent=None): super(Graph, self).__init__(parent) @@ -199,7 +198,7 @@ class Graph(BaseObject): # Edges: use dst attribute as unique key since it can only have one input connection self._edges = DictModel(keyAttrName='dst', parent=self) self._compatibilityNodes = DictModel(keyAttrName='name', parent=self) - self.cacheDir = meshroom.core.defaultCacheFolder + self._cacheDir = '' self._filepath = '' self._fileDateVersion = 0 self.header = {} @@ -1354,7 +1353,7 @@ class Graph(BaseObject): def _unsetFilepath(self): self._filepath = "" self.name = "" - self.cacheDir = meshroom.core.defaultCacheFolder + self.cacheDir = "" self.filepathChanged.emit() def updateInternals(self, startNodes=None, force=False): diff --git a/meshroom/env.py b/meshroom/env.py index 46ce30e9..ddd48577 100644 --- a/meshroom/env.py +++ b/meshroom/env.py @@ -12,9 +12,9 @@ import os from dataclasses import dataclass from enum import Enum import sys +import tempfile from typing import Any, Type - meshroomFolder = os.path.dirname(__file__) @dataclass @@ -46,6 +46,8 @@ class EnvVar(Enum): MESHROOM_NODES_PATH = VarDefinition(str, "", "Paths to set of nodes folders") MESHROOM_SUBMITTERS_PATH = VarDefinition(str, "", "Paths to set of submitters folders") MESHROOM_PIPELINE_TEMPLATES_PATH = VarDefinition(str, "", "Paths to et of pipeline templates folders") + MESHROOM_TEMP_PATH = VarDefinition(str, tempfile.gettempdir(), "Path to the temporary folder") + @staticmethod def get(envVar: "EnvVar") -> Any: diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index a41430b2..d5062bf8 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -514,6 +514,14 @@ class UIGraph(QObject): # => force re-evaluation of monitored status files paths self.updateChunkMonitor(self._sortedDFSChunks) + @Slot() + def saveAsTemp(self): + from meshroom.env import EnvVar + from datetime import datetime + tempFolder = EnvVar.get(EnvVar.MESHROOM_TEMP_PATH) + timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M") + self._saveAs(os.path.join(tempFolder, f"meshroom_{timestamp}.mg")) + @Slot() def save(self): self._graph.save() @@ -531,6 +539,7 @@ class UIGraph(QObject): @Slot(list) def execute(self, nodes: Optional[Union[list[Node], Node]] = None): nodes = [nodes] if not isinstance(nodes, Iterable) and nodes else nodes + self.save() # always save the graph before computing self._taskManager.compute(self._graph, nodes) self.updateLockedUndoStack() # explicitly call the update while it is already computing diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml index ef91bcca..59e04d12 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -134,6 +134,8 @@ Page { id: saveFileDialog options: Platform.FileDialog.DontUseNativeDialog + property var _callback: undefined + signal closed(var result) title: "Save File" @@ -150,8 +152,28 @@ Page { _reconstruction.saveAs(currentFile) MeshroomApp.addRecentProjectFile(currentFile.toString()) closed(Platform.Dialog.Accepted) + fireCallback(Platform.Dialog.Accepted) + } + onRejected: { + closed(Platform.Dialog.Rejected) + fireCallback(Platform.Dialog.Rejected) + } + + function fireCallback(rc) + { + // Call the callback and reset it + if (_callback) + _callback(rc) + _callback = undefined + } + + // Open the unsaved dialog warning with an optional + // callback to fire when the dialog is accepted/discarded + function prompt(callback) + { + _callback = callback + open() } - onRejected: closed(Platform.Dialog.Rejected) } Platform.FileDialog { @@ -218,8 +240,6 @@ Page { Item { id: computeManager - property bool warnIfUnsaved: true - // Evaluate if graph computation can be submitted externally property bool canSubmit: _reconstruction ? _reconstruction.canSubmit // current setup allows to compute externally @@ -227,7 +247,7 @@ Page { false function compute(nodes, force) { - if (!force && warnIfUnsaved && !_reconstruction.graph.filepath) { + if (!force && !_reconstruction.graph.filepath) { unsavedComputeDialog.selectedNodes = nodes; unsavedComputeDialog.open(); } @@ -339,29 +359,28 @@ Page { parent: Overlay.overlay preset: "Warning" title: "Unsaved Project" - text: "Data will be computed in the default cache folder if project remains unsaved." - detailedText: "Default cache folder: " + (_reconstruction ? _reconstruction.graph.cacheDir : "unknown") - helperText: "Save project first?" + text: "Saving the project is required." + helperText: "Choose a location to save the project, or use the default temporary path." standardButtons: Dialog.Discard | Dialog.Cancel | Dialog.Save - CheckBox { - Layout.alignment: Qt.AlignRight - text: "Don't ask again for this session" - padding: 0 - onToggled: computeManager.warnIfUnsaved = !checked - } - Component.onCompleted: { // Set up discard button text - standardButton(Dialog.Discard).text = "Continue without Saving" + standardButton(Dialog.Discard).text = "Continue in Temp Folder" + standardButton(Dialog.Save).text = "Save As" } onDiscarded: { + _reconstruction.saveAsTemp() close() computeManager.compute(selectedNodes, true) } - onAccepted: saveAsAction.trigger() + onAccepted: { + initFileDialogFolder(saveFileDialog) + saveFileDialog.prompt(function(rc) { + computeManager.compute(selectedNodes, true) + }) + } } MessageDialog { @@ -444,14 +463,10 @@ Page { } // Open "Save As" dialog else { - saveFileDialog.open() - function _callbackWrapper(rc) { + saveFileDialog.prompt(function(rc) { if (rc === Platform.Dialog.Accepted) fireCallback() - - saveFileDialog.closed.disconnect(_callbackWrapper) - } - saveFileDialog.closed.connect(_callbackWrapper) + }) } } @@ -463,8 +478,8 @@ Page { _callback = undefined } - /// Open the unsaved dialog warning with an optional - /// callback to fire when the dialog is accepted/discarded + // Open the unsaved dialog warning with an optional + // callback to fire when the dialog is accepted/discarded function prompt(callback) { _callback = callback