Automatically save the project when computing or submitting to renderfarm

If the project is not saved at all, it will suggest to save it manually
or to define a project in a temporary folder using date/time for the
project name.
This commit is contained in:
Fabien Castan 2025-04-12 19:49:11 +02:00
parent db8fd02aeb
commit 008d6c75ee
6 changed files with 53 additions and 44 deletions

View file

@ -31,7 +31,6 @@ logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=log
sessionUid = str(uuid.uuid1()) sessionUid = str(uuid.uuid1())
cacheFolderName = 'MeshroomCache' cacheFolderName = 'MeshroomCache'
defaultCacheFolder = os.environ.get('MESHROOM_CACHE', os.path.join(tempfile.gettempdir(), cacheFolderName))
nodesDesc = {} nodesDesc = {}
submitters = {} submitters = {}
pipelineTemplates = {} pipelineTemplates = {}

View file

@ -25,18 +25,6 @@ class MrNodeType(enum.Enum):
COMMANDLINE = enum.auto() COMMANDLINE = enum.auto()
INPUT = 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): class BaseNode(object):
""" """
@ -259,9 +247,6 @@ class Node(BaseNode):
return MrNodeType.NODE return MrNodeType.NODE
def processChunkInEnvironment(self, chunk): 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" meshroomComputeCmd = f"python {_MESHROOM_COMPUTE} {chunk.node.graph.filepath} --node {chunk.node.name} --extern --inCurrentEnv"
if len(chunk.node.getChunks()) > 1: if len(chunk.node.getChunks()) > 1:
meshroomComputeCmd += f" --iteration {chunk.range.iteration}" meshroomComputeCmd += f" --iteration {chunk.range.iteration}"

View file

@ -182,7 +182,6 @@ class Graph(BaseObject):
edges = {B.input: A.output, C.input: B.output,} edges = {B.input: A.output, C.input: B.output,}
""" """
_cacheDir = ""
def __init__(self, name, parent=None): def __init__(self, name, parent=None):
super(Graph, self).__init__(parent) 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 # Edges: use dst attribute as unique key since it can only have one input connection
self._edges = DictModel(keyAttrName='dst', parent=self) self._edges = DictModel(keyAttrName='dst', parent=self)
self._compatibilityNodes = DictModel(keyAttrName='name', parent=self) self._compatibilityNodes = DictModel(keyAttrName='name', parent=self)
self.cacheDir = meshroom.core.defaultCacheFolder self._cacheDir = ''
self._filepath = '' self._filepath = ''
self._fileDateVersion = 0 self._fileDateVersion = 0
self.header = {} self.header = {}
@ -1354,7 +1353,7 @@ class Graph(BaseObject):
def _unsetFilepath(self): def _unsetFilepath(self):
self._filepath = "" self._filepath = ""
self.name = "" self.name = ""
self.cacheDir = meshroom.core.defaultCacheFolder self.cacheDir = ""
self.filepathChanged.emit() self.filepathChanged.emit()
def updateInternals(self, startNodes=None, force=False): def updateInternals(self, startNodes=None, force=False):

View file

@ -12,9 +12,9 @@ import os
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
import sys import sys
import tempfile
from typing import Any, Type from typing import Any, Type
meshroomFolder = os.path.dirname(__file__) meshroomFolder = os.path.dirname(__file__)
@dataclass @dataclass
@ -46,6 +46,8 @@ class EnvVar(Enum):
MESHROOM_NODES_PATH = VarDefinition(str, "", "Paths to set of nodes folders") MESHROOM_NODES_PATH = VarDefinition(str, "", "Paths to set of nodes folders")
MESHROOM_SUBMITTERS_PATH = VarDefinition(str, "", "Paths to set of submitters 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_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 @staticmethod
def get(envVar: "EnvVar") -> Any: def get(envVar: "EnvVar") -> Any:

View file

@ -514,6 +514,14 @@ class UIGraph(QObject):
# => force re-evaluation of monitored status files paths # => force re-evaluation of monitored status files paths
self.updateChunkMonitor(self._sortedDFSChunks) 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() @Slot()
def save(self): def save(self):
self._graph.save() self._graph.save()
@ -531,6 +539,7 @@ class UIGraph(QObject):
@Slot(list) @Slot(list)
def execute(self, nodes: Optional[Union[list[Node], Node]] = None): def execute(self, nodes: Optional[Union[list[Node], Node]] = None):
nodes = [nodes] if not isinstance(nodes, Iterable) and nodes else nodes 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._taskManager.compute(self._graph, nodes)
self.updateLockedUndoStack() # explicitly call the update while it is already computing self.updateLockedUndoStack() # explicitly call the update while it is already computing

View file

@ -134,6 +134,8 @@ Page {
id: saveFileDialog id: saveFileDialog
options: Platform.FileDialog.DontUseNativeDialog options: Platform.FileDialog.DontUseNativeDialog
property var _callback: undefined
signal closed(var result) signal closed(var result)
title: "Save File" title: "Save File"
@ -150,8 +152,28 @@ Page {
_reconstruction.saveAs(currentFile) _reconstruction.saveAs(currentFile)
MeshroomApp.addRecentProjectFile(currentFile.toString()) MeshroomApp.addRecentProjectFile(currentFile.toString())
closed(Platform.Dialog.Accepted) 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 { Platform.FileDialog {
@ -218,8 +240,6 @@ Page {
Item { Item {
id: computeManager id: computeManager
property bool warnIfUnsaved: true
// Evaluate if graph computation can be submitted externally // Evaluate if graph computation can be submitted externally
property bool canSubmit: _reconstruction ? property bool canSubmit: _reconstruction ?
_reconstruction.canSubmit // current setup allows to compute externally _reconstruction.canSubmit // current setup allows to compute externally
@ -227,7 +247,7 @@ Page {
false false
function compute(nodes, force) { function compute(nodes, force) {
if (!force && warnIfUnsaved && !_reconstruction.graph.filepath) { if (!force && !_reconstruction.graph.filepath) {
unsavedComputeDialog.selectedNodes = nodes; unsavedComputeDialog.selectedNodes = nodes;
unsavedComputeDialog.open(); unsavedComputeDialog.open();
} }
@ -339,29 +359,28 @@ Page {
parent: Overlay.overlay parent: Overlay.overlay
preset: "Warning" preset: "Warning"
title: "Unsaved Project" title: "Unsaved Project"
text: "Data will be computed in the default cache folder if project remains unsaved." text: "Saving the project is required."
detailedText: "Default cache folder: " + (_reconstruction ? _reconstruction.graph.cacheDir : "unknown") helperText: "Choose a location to save the project, or use the default temporary path."
helperText: "Save project first?"
standardButtons: Dialog.Discard | Dialog.Cancel | Dialog.Save 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: { Component.onCompleted: {
// Set up discard button text // 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: { onDiscarded: {
_reconstruction.saveAsTemp()
close() close()
computeManager.compute(selectedNodes, true) computeManager.compute(selectedNodes, true)
} }
onAccepted: saveAsAction.trigger() onAccepted: {
initFileDialogFolder(saveFileDialog)
saveFileDialog.prompt(function(rc) {
computeManager.compute(selectedNodes, true)
})
}
} }
MessageDialog { MessageDialog {
@ -444,14 +463,10 @@ Page {
} }
// Open "Save As" dialog // Open "Save As" dialog
else { else {
saveFileDialog.open() saveFileDialog.prompt(function(rc) {
function _callbackWrapper(rc) {
if (rc === Platform.Dialog.Accepted) if (rc === Platform.Dialog.Accepted)
fireCallback() fireCallback()
})
saveFileDialog.closed.disconnect(_callbackWrapper)
}
saveFileDialog.closed.connect(_callbackWrapper)
} }
} }
@ -463,8 +478,8 @@ Page {
_callback = undefined _callback = undefined
} }
/// Open the unsaved dialog warning with an optional // Open the unsaved dialog warning with an optional
/// callback to fire when the dialog is accepted/discarded // callback to fire when the dialog is accepted/discarded
function prompt(callback) function prompt(callback)
{ {
_callback = callback _callback = callback