Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,47 @@ def save(self, filepath=None, setupProjectFile=True, template=False):
self._save(filepath=filepath, setupProjectFile=setupProjectFile, template=template)
finally:
self._saving = False

def _generateNextPath(self):
"""
Generate the filename for the next version
- scene.mg -> scene1.mg
- scene1.mg -> scene2.mg
- scene_001.mg -> scene_002.mg (preserves zero-padding)
- scene1.mg and scene2.mg exists -> scene3.mg
"""
path = Path(self._filepath)
stem, ext = path.stem, path.suffix
# Match name and version number at the end
versionMatch = re.match(r'^(.+?)(\d+)$', stem)
if versionMatch:
stemBase, versionStr = versionMatch.group(1), versionMatch.group(2)
version = int(versionStr) + 1
# Preserve zero-padding from original
padding = len(versionStr)
else:
stemBase, version, padding = stem, 1, 1
# Find an available name
while True:
# Format version number with appropriate padding
versionStr = str(version).zfill(padding)
pathCandidate = path.parent / f"{stemBase}{versionStr}{ext}"
if not pathCandidate.exists():
return str(pathCandidate)
version += 1

def saveAsNewVersion(self):
"""
Increase the version of the file and save
"""
# Generate the new version path
path = self._generateNextPath()
# Update the saving flag indicating that the current graph is being saved
self._saving = True
try:
self._save(filepath=path)
finally:
self._saving = False
Comment thread
Alxiice marked this conversation as resolved.

def _save(self, filepath=None, setupProjectFile=True, template=False):
path = filepath or self._filepath
Expand Down
5 changes: 5 additions & 0 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,11 @@ def save(self):
self._graph.save()
self._undoStack.setClean()

@Slot()
def saveAsNewVersion(self):
self._graph.saveAsNewVersion()
Comment thread
Alxiice marked this conversation as resolved.
self._undoStack.setClean()

@Slot()
def updateLockedUndoStack(self):
if self.isComputingLocally():
Expand Down
10 changes: 10 additions & 0 deletions meshroom/ui/qml/Application.qml
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,16 @@ Page {
saveFileDialog.open()
}
}
Action {
id: saveNewVersionAction
text: "Save New Version"
shortcut: "Ctrl+Alt+S"
enabled: _reconstruction && _reconstruction.graph && _reconstruction.graph.filepath
onTriggered: {
_reconstruction.saveAsNewVersion()
MeshroomApp.addRecentProjectFile(_reconstruction.graph.filepath)
}
}
MenuSeparator { }
Action {
id: importImagesAction
Expand Down
44 changes: 44 additions & 0 deletions tests/test_graphIO.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
import os
from textwrap import dedent
from pathlib import Path

from meshroom.core import desc
from meshroom.core.graph import Graph
Expand Down Expand Up @@ -43,6 +45,10 @@ class NodeWithListAttributes(desc.Node):
]


def assertPathsAreEqual(pathA, pathB):
return Path(pathA).resolve().as_posix() == Path(pathB).resolve().as_posix()


def compareGraphsContent(graphA: Graph, graphB: Graph) -> bool:
"""Returns whether the content (node and deges) of two graphs are considered identical.

Expand Down Expand Up @@ -214,6 +220,44 @@ def test_importingDifferentNodeVersionCreatesCompatibilityNodes(self, graphSaved
assert otherGraph.node(node.name).issue is CompatibilityIssue.VersionConflict


class TestGraphSave:
def test_generateNextPath(self, graphSavedOnDisk):
graph: Graph = graphSavedOnDisk
root = os.path.dirname(graph._filepath)
# Files with no version number (e.g., "scene.mg" -> "scene1.mg")
graph._filepath = os.path.join(root, "scene.mg")
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene1.mg"))
# Files with existing version numbers (e.g., "scene1.mg" -> "scene2.mg")
graph._filepath = os.path.join(root, "scene_1.mg")
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene_2.mg"))
# Edge cases like filenames that are purely numeric (e.g., "123.mg")
# Also test that the padding is kept ("001" -> "002" and not "2")
graph._filepath = os.path.join(root, "0123.mg")
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "0124.mg"))
graph._filepath = os.path.join(root, "scene_001.mg")
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene_002.mg"))
# Files where the next version already exists (e.g., "scene1.mg" when "scene2.mg" exists -> "scene3.mg")
graph._filepath = os.path.join(root, "scene1.mg")
open(os.path.join(root, "scene2.mg"), 'a').close()
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene3.mg"))

def test_saveAsNewVersion(self, tmp_path):
graph = Graph("")
with registeredNodeTypes([SimpleNode]):
# Create scene
nodeA = graph.addNewNode(SimpleNode.__name__)
scenePath = os.path.join(tmp_path, "scene.mg")
graph._filepath = scenePath
graph.save()
assert os.path.exists(scenePath)
# Modify scene
nodeB = graph.addNewNode(SimpleNode.__name__)
nodeA.output.connectTo(nodeB.input)
graph.saveAsNewVersion()
newScenePath = os.path.join(tmp_path, "scene1.mg")
assert os.path.exists(newScenePath)


class TestGraphPartialSerialization:
def test_emptyGraph(self):
graph = Graph("")
Expand Down
Loading