Skip to content

Commit c939290

Browse files
authored
Merge pull request #2980 from alicevision/dev/save_increment_version
[ui/core] Add a menu to increment file version
2 parents da30eaf + 911f199 commit c939290

File tree

4 files changed

+100
-0
lines changed

4 files changed

+100
-0
lines changed

meshroom/core/graph.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,47 @@ def save(self, filepath=None, setupProjectFile=True, template=False):
13801380
self._save(filepath=filepath, setupProjectFile=setupProjectFile, template=template)
13811381
finally:
13821382
self._saving = False
1383+
1384+
def _generateNextPath(self):
1385+
"""
1386+
Generate the filename for the next version
1387+
- scene.mg -> scene1.mg
1388+
- scene1.mg -> scene2.mg
1389+
- scene_001.mg -> scene_002.mg (preserves zero-padding)
1390+
- scene1.mg and scene2.mg exists -> scene3.mg
1391+
"""
1392+
path = Path(self._filepath)
1393+
stem, ext = path.stem, path.suffix
1394+
# Match name and version number at the end
1395+
versionMatch = re.match(r'^(.+?)(\d+)$', stem)
1396+
if versionMatch:
1397+
stemBase, versionStr = versionMatch.group(1), versionMatch.group(2)
1398+
version = int(versionStr) + 1
1399+
# Preserve zero-padding from original
1400+
padding = len(versionStr)
1401+
else:
1402+
stemBase, version, padding = stem, 1, 1
1403+
# Find an available name
1404+
while True:
1405+
# Format version number with appropriate padding
1406+
versionStr = str(version).zfill(padding)
1407+
pathCandidate = path.parent / f"{stemBase}{versionStr}{ext}"
1408+
if not pathCandidate.exists():
1409+
return str(pathCandidate)
1410+
version += 1
1411+
1412+
def saveAsNewVersion(self):
1413+
"""
1414+
Increase the version of the file and save
1415+
"""
1416+
# Generate the new version path
1417+
path = self._generateNextPath()
1418+
# Update the saving flag indicating that the current graph is being saved
1419+
self._saving = True
1420+
try:
1421+
self._save(filepath=path)
1422+
finally:
1423+
self._saving = False
13831424

13841425
def _save(self, filepath=None, setupProjectFile=True, template=False):
13851426
path = filepath or self._filepath

meshroom/ui/graph.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,11 @@ def save(self):
547547
self._graph.save()
548548
self._undoStack.setClean()
549549

550+
@Slot()
551+
def saveAsNewVersion(self):
552+
self._graph.saveAsNewVersion()
553+
self._undoStack.setClean()
554+
550555
@Slot()
551556
def updateLockedUndoStack(self):
552557
if self.isComputingLocally():

meshroom/ui/qml/Application.qml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,16 @@ Page {
775775
saveFileDialog.open()
776776
}
777777
}
778+
Action {
779+
id: saveNewVersionAction
780+
text: "Save New Version"
781+
shortcut: "Ctrl+Alt+S"
782+
enabled: _reconstruction && _reconstruction.graph && _reconstruction.graph.filepath
783+
onTriggered: {
784+
_reconstruction.saveAsNewVersion()
785+
MeshroomApp.addRecentProjectFile(_reconstruction.graph.filepath)
786+
}
787+
}
778788
MenuSeparator { }
779789
Action {
780790
id: importImagesAction

tests/test_graphIO.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import json
2+
import os
23
from textwrap import dedent
4+
from pathlib import Path
35

46
from meshroom.core import desc
57
from meshroom.core.graph import Graph
@@ -43,6 +45,10 @@ class NodeWithListAttributes(desc.Node):
4345
]
4446

4547

48+
def assertPathsAreEqual(pathA, pathB):
49+
return Path(pathA).resolve().as_posix() == Path(pathB).resolve().as_posix()
50+
51+
4652
def compareGraphsContent(graphA: Graph, graphB: Graph) -> bool:
4753
"""Returns whether the content (node and deges) of two graphs are considered identical.
4854
@@ -214,6 +220,44 @@ def test_importingDifferentNodeVersionCreatesCompatibilityNodes(self, graphSaved
214220
assert otherGraph.node(node.name).issue is CompatibilityIssue.VersionConflict
215221

216222

223+
class TestGraphSave:
224+
def test_generateNextPath(self, graphSavedOnDisk):
225+
graph: Graph = graphSavedOnDisk
226+
root = os.path.dirname(graph._filepath)
227+
# Files with no version number (e.g., "scene.mg" -> "scene1.mg")
228+
graph._filepath = os.path.join(root, "scene.mg")
229+
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene1.mg"))
230+
# Files with existing version numbers (e.g., "scene1.mg" -> "scene2.mg")
231+
graph._filepath = os.path.join(root, "scene_1.mg")
232+
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene_2.mg"))
233+
# Edge cases like filenames that are purely numeric (e.g., "123.mg")
234+
# Also test that the padding is kept ("001" -> "002" and not "2")
235+
graph._filepath = os.path.join(root, "0123.mg")
236+
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "0124.mg"))
237+
graph._filepath = os.path.join(root, "scene_001.mg")
238+
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene_002.mg"))
239+
# Files where the next version already exists (e.g., "scene1.mg" when "scene2.mg" exists -> "scene3.mg")
240+
graph._filepath = os.path.join(root, "scene1.mg")
241+
open(os.path.join(root, "scene2.mg"), 'a').close()
242+
assertPathsAreEqual(graph._generateNextPath(), os.path.join(root, "scene3.mg"))
243+
244+
def test_saveAsNewVersion(self, tmp_path):
245+
graph = Graph("")
246+
with registeredNodeTypes([SimpleNode]):
247+
# Create scene
248+
nodeA = graph.addNewNode(SimpleNode.__name__)
249+
scenePath = os.path.join(tmp_path, "scene.mg")
250+
graph._filepath = scenePath
251+
graph.save()
252+
assert os.path.exists(scenePath)
253+
# Modify scene
254+
nodeB = graph.addNewNode(SimpleNode.__name__)
255+
nodeA.output.connectTo(nodeB.input)
256+
graph.saveAsNewVersion()
257+
newScenePath = os.path.join(tmp_path, "scene1.mg")
258+
assert os.path.exists(newScenePath)
259+
260+
217261
class TestGraphPartialSerialization:
218262
def test_emptyGraph(self):
219263
graph = Graph("")

0 commit comments

Comments
 (0)