Skip to content
Open
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
11 changes: 8 additions & 3 deletions meshroom/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
pass

from meshroom.core.plugins import NodePlugin, NodePluginManager, Plugin, processEnvFactory, formatNodeDescriptionErrorMessage
from meshroom.core.files import MESHROOM_PROJECT_EXTENSION, MESHROOM_TEMPLATE_EXTENSION, hasExtension, isTemplateFile
from meshroom.core.submitter import BaseSubmitter
from meshroom.env import EnvVar, meshroomFolder
from . import desc
Expand Down Expand Up @@ -425,9 +426,13 @@ def loadPipelineTemplates(folder: str):
if not os.path.isdir(folder):
logging.error(f"Pipeline templates folder '{folder}' does not exist.")
return
for file in os.listdir(folder):
if file.endswith(".mg") and file not in pipelineTemplates:
pipelineTemplates[os.path.splitext(file)[0]] = os.path.join(folder, file)
for file in sorted(os.listdir(folder)):
filepath = os.path.join(folder, file)
templateName = Path(file).stem
if hasExtension(filepath, (MESHROOM_TEMPLATE_EXTENSION,)):
pipelineTemplates[templateName] = filepath
elif hasExtension(filepath, (MESHROOM_PROJECT_EXTENSION,)) and isTemplateFile(filepath):
pipelineTemplates.setdefault(templateName, filepath)


def initNodes():
Expand Down
41 changes: 41 additions & 0 deletions meshroom/core/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
from pathlib import Path


MESHROOM_PROJECT_EXTENSION = ".mg"
MESHROOM_TEMPLATE_EXTENSION = ".mgt"
MESHROOM_LEGACY_TEMPLATE_EXTENSION = MESHROOM_PROJECT_EXTENSION


def extensionLower(filepath) -> str:
return Path(filepath).suffix.lower()


def hasExtension(filepath, extensions: tuple[str, ...]) -> bool:
return extensionLower(filepath) in extensions


def withExtension(filepath, extension: str) -> str:
"""Return filepath with the requested extension if it has no matching suffix."""
filepath = str(filepath)
if extensionLower(filepath) != extension:
filepath += extension
return filepath


def isTemplateGraphData(graphData: dict) -> bool:
return bool(graphData.get("header", {}).get("template", False))


def isTemplateFile(filepath) -> bool:
"""Return whether filepath should be opened through the template flow."""
path = Path(filepath)
if extensionLower(path) == MESHROOM_TEMPLATE_EXTENSION:
return True
if extensionLower(path) != MESHROOM_LEGACY_TEMPLATE_EXTENSION:
return False
try:
with open(path) as file:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Specify encoding="utf-8" when opening the file to prevent potential UnicodeDecodeError on platforms where the default system encoding is not UTF-8 (such as Windows).

Suggested change
with open(path) as file:
with open(path, encoding="utf-8") as file:

return isTemplateGraphData(json.load(file))
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
return False
19 changes: 18 additions & 1 deletion meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from meshroom.core import submitters
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
from meshroom.core.exception import GraphCompatibilityError, InvalidEdgeError, StopGraphVisit, StopBranchVisit, CyclicDependencyError
from meshroom.core.files import MESHROOM_PROJECT_EXTENSION, isTemplateFile
from meshroom.core.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
from meshroom.core.nodeFactory import nodeFactory, getNodeConstructor
Expand Down Expand Up @@ -188,7 +189,7 @@ def generateTempProjectFilepath(tmpFolder=None):
from meshroom.env import EnvVar
tmpFolder = EnvVar.get(EnvVar.MESHROOM_TEMP_PATH)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M")
return os.path.join(tmpFolder, f"meshroom_{timestamp}.mg")
return os.path.join(tmpFolder, f"meshroom_{timestamp}{MESHROOM_PROJECT_EXTENSION}")


class Graph(BaseObject):
Expand Down Expand Up @@ -227,6 +228,7 @@ def __init__(self, name: str = "", parent: BaseObject = None):
self._relativeCacheDir: str = ""
self._cacheDir: str = ""
self._filepath: str = ""
self._templateFilepath: str = ""
self._fileDateVersion = 0
self.header = {}

Expand Down Expand Up @@ -1552,6 +1554,7 @@ def _setFilepath(self, filepath):
if self._filepath == newFilepath:
return
self._filepath = newFilepath
self._setTemplateFilepath("")
# For now:
# * cache folder is located next to the graph file
# * graph name if the basename of the graph file
Expand All @@ -1564,10 +1567,22 @@ def _setFilepath(self, filepath):

def _unsetFilepath(self):
self._filepath = ""
self._setTemplateFilepath("")
self.name = ""
self.cacheDir = ""
self.filepathChanged.emit()

@Slot(str)
def setTemplateFilepath(self, filepath):
self._setTemplateFilepath(filepath)

def _setTemplateFilepath(self, filepath):
newFilepath = Path(filepath).as_posix() if filepath else ""
if self._templateFilepath == newFilepath:
return
self._templateFilepath = newFilepath
self.templateFilepathChanged.emit()

def updateInternals(self, startNodes=None, force=False):
nodes, edges = self.dfsOnFinish(startNodes=startNodes)
for node in nodes:
Expand Down Expand Up @@ -1810,6 +1825,8 @@ def setVerbose(self, v):
edges = Property(BaseObject, edges.fget, constant=True)
filepathChanged = Signal()
filepath = Property(str, lambda self: self._filepath, notify=filepathChanged)
templateFilepathChanged = Signal()
templateFilepath = Property(str, lambda self: self._templateFilepath, notify=templateFilepathChanged)
isSaving = Property(bool, isSaving.fget, constant=True)
fileReleaseVersion = Property(str, lambda self: self.header.get(GraphIO.Keys.ReleaseVersion, "0.0"),
notify=filepathChanged)
Expand Down
13 changes: 9 additions & 4 deletions meshroom/core/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from meshroom.common import BaseObject
from meshroom.core import desc
from meshroom.core.desc.attribute import ValueTypeErrors
from meshroom.core.files import MESHROOM_PROJECT_EXTENSION, MESHROOM_TEMPLATE_EXTENSION, hasExtension, isTemplateFile
from meshroom import _MESHROOM_ROOT
from meshroom.core.desc.node import _MESHROOM_COMPUTE_DEPS

Expand Down Expand Up @@ -286,7 +287,7 @@ class Plugin(BaseObject):
path: the absolute path of the plugin
nodePlugins: dictionary mapping the name of a node plugin contained in the plugin
to its corresponding NodePlugin object
templates: dictionary mapping the name of templates (.mg files) associated to the plugin
templates: dictionary mapping the name of templates associated to the plugin
with their absolute paths
configEnv: the environment variables and their values, as described in the plugin's
configuration file
Expand Down Expand Up @@ -403,9 +404,13 @@ def loadTemplates(self):
before being filled again.
"""
self._templates.clear()
for file in os.listdir(self.path):
if file.endswith(".mg"):
self._templates[os.path.splitext(file)[0]] = os.path.join(self.path, file)
for file in sorted(os.listdir(self.path)):
filepath = os.path.join(self.path, file)
templateName = Path(file).stem
if hasExtension(filepath, (MESHROOM_TEMPLATE_EXTENSION,)):
self._templates[templateName] = filepath
elif hasExtension(filepath, (MESHROOM_PROJECT_EXTENSION,)) and isTemplateFile(filepath):
self._templates.setdefault(templateName, filepath)

def loadConfig(self):
"""
Expand Down
13 changes: 10 additions & 3 deletions meshroom/multiview.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os

from meshroom.core.files import MESHROOM_PROJECT_EXTENSION, MESHROOM_TEMPLATE_EXTENSION, hasExtension as hasFileExtension

# Supported image extensions
imageExtensions = [
# bmp:
Expand Down Expand Up @@ -67,14 +69,15 @@
'.mxf',
]
panoramaInfoExtensions = ['.xml']
meshroomSceneExtensions = ['.mg']
meshroomSceneExtensions = [MESHROOM_PROJECT_EXTENSION]
meshroomTemplateExtensions = [MESHROOM_TEMPLATE_EXTENSION]


def hasExtension(filepath, extensions):
""" Return whether filepath is one of the following extensions. """
if os.path.isdir(filepath):
return False
return os.path.splitext(filepath)[1].lower() in extensions
return hasFileExtension(filepath, extensions)


class FilesByType:
Expand All @@ -83,16 +86,18 @@ def __init__(self):
self.videos = []
self.panoramaInfo = []
self.meshroomScenes = []
self.meshroomTemplates = []
self.other = []

def __bool__(self):
return self.images or self.videos or self.panoramaInfo or self.meshroomScenes
return self.images or self.videos or self.panoramaInfo or self.meshroomScenes or self.meshroomTemplates

def extend(self, other):
self.images.extend(other.images)
self.videos.extend(other.videos)
self.panoramaInfo.extend(other.panoramaInfo)
self.meshroomScenes.extend(other.meshroomScenes)
self.meshroomTemplates.extend(other.meshroomTemplates)
self.other.extend(other.other)

def addFile(self, file):
Expand All @@ -104,6 +109,8 @@ def addFile(self, file):
self.panoramaInfo.append(file)
elif hasExtension(file, meshroomSceneExtensions):
self.meshroomScenes.append(file)
elif hasExtension(file, meshroomTemplateExtensions):
self.meshroomTemplates.append(file)
else:
self.other.append(file)

Expand Down
99 changes: 78 additions & 21 deletions meshroom/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import meshroom
from meshroom.core import pluginManager
from meshroom.core.files import isTemplateFile
from meshroom.core.submitter import BaseSubmitter
from meshroom.core.taskManager import TaskManager
from meshroom.common import Property, Variant, Signal, Slot
Expand Down Expand Up @@ -261,6 +262,7 @@ def __init__(self, inputArgs):

# Initialize the list of recent project files
self._recentProjectFiles = self._getRecentProjectFilesFromSettings()
self._recentTemplateFiles = self._getRecentTemplateFilesFromSettings()
# Flag set to True if, for all the project files in the list, thumbnails have been retrieved when they
# are available. If set to False, then all the paths in the list are accurate, but some thumbnails might
# be retrievable
Expand Down Expand Up @@ -328,7 +330,10 @@ def __init__(self, inputArgs):
if args.project:
args.project = os.path.abspath(args.project)
self._activeProject.load(args.project)
self.addRecentProjectFile(args.project)
if self._activeProject.graph.filepath:
self.addRecentProjectFile(args.project)
else:
self.addRecentTemplateFile(args.project)
elif args.new:
self._activeProject.new()
elif args.latest or args.latest2 or args.latest3:
Expand Down Expand Up @@ -464,6 +469,27 @@ def _getRecentProjectFilesFromSettings(self) -> list[dict[str, str]]:
settings.endGroup()
return projects

def _getRecentTemplateFilesFromSettings(self) -> list[dict[str, str]]:
"""
Read the list of recent template files from QSettings.

Returns:
The list containing dictionaries of the form {"path": "/path/to/template/file", "status": 1}.
"""
templates = []
settings = QSettings()
settings.beginGroup("RecentFiles")
size = settings.beginReadArray("Templates")
for i in range(size):
settings.setArrayIndex(i)
path = settings.value("filepath")
if path:
fileStatus = FileStatus.EXISTS if os.path.isfile(path) else FileStatus.MISSING
templates.append({"path": path, "status": fileStatus.value})
settings.endArray()
settings.endGroup()
return templates

@Slot()
def updateRecentProjectFilesThumbnails(self) -> None:
"""
Expand Down Expand Up @@ -495,16 +521,7 @@ def addRecentProjectFile(self, projectFile) -> None:
Args:
projectFile (str or QUrl): path to the project file to add to the list
"""
if not isinstance(projectFile, (QUrl, str)):
raise TypeError(f"Unexpected data type: {projectFile.__class__}")
if isinstance(projectFile, QUrl):
projectFileNorm = projectFile.toLocalFile()
if not projectFileNorm:
projectFileNorm = projectFile.toString()
else:
projectFileNorm = QUrl(projectFile).toLocalFile()
if not projectFileNorm:
projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile()
projectFileNorm = self._normalizeFilepath(projectFile)

# Get the list of recent projects without re-reading the QSettings
projects = self._recentProjectFiles
Expand Down Expand Up @@ -539,6 +556,39 @@ def addRecentProjectFile(self, projectFile) -> None:
self._updatedRecentProjectFilesThumbnails = False # Thumbnails may not be up-to-date
self.recentProjectFilesChanged.emit()

@Slot(str)
@Slot(QUrl)
def addRecentTemplateFile(self, templateFile) -> None:
"""
Add a template file to the list of recent template files.
"""
templateFileNorm = self._normalizeFilepath(templateFile)

templates = self._recentTemplateFiles
filepaths = [t["path"] for t in templates]
if templateFileNorm in filepaths:
idx = filepaths.index(templateFileNorm)
del templates[idx]

templates.insert(0, {"path": templateFileNorm, "status": FileStatus.EXISTS.value})

maxNbTemplates = 40
if len(templates) > maxNbTemplates:
templates = templates[0:maxNbTemplates]

settings = QSettings()
settings.beginGroup("RecentFiles")
settings.beginWriteArray("Templates")
for i, t in enumerate(templates):
settings.setArrayIndex(i)
settings.setValue("filepath", t["path"])
settings.endArray()
settings.endGroup()
settings.sync()

self._recentTemplateFiles = templates
self.recentTemplateFilesChanged.emit()

@Slot(str)
@Slot(QUrl)
def removeRecentProjectFile(self, projectFile) -> None:
Expand All @@ -547,16 +597,7 @@ def removeRecentProjectFile(self, projectFile) -> None:
If the provided filepath is not already present in the list of recent project files, nothing is done.
Otherwise, it is effectively removed and the QSettings are updated accordingly.
"""
if not isinstance(projectFile, (QUrl, str)):
raise TypeError(f"Unexpected data type: {projectFile.__class__}")
if isinstance(projectFile, QUrl):
projectFileNorm = projectFile.toLocalFile()
if not projectFileNorm:
projectFileNorm = projectFile.toString()
else:
projectFileNorm = QUrl(projectFile).toLocalFile()
if not projectFileNorm:
projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile()
projectFileNorm = self._normalizeFilepath(projectFile)

# Get the list of recent projects without re-reading the QSettings
projects = self._recentProjectFiles
Expand Down Expand Up @@ -584,6 +625,20 @@ def removeRecentProjectFile(self, projectFile) -> None:
self._recentProjectFiles = projects
self.recentProjectFilesChanged.emit()

@staticmethod
def _normalizeFilepath(filepath) -> str:
if not isinstance(filepath, (QUrl, str)):
raise TypeError(f"Unexpected data type: {filepath.__class__}")
if isinstance(filepath, QUrl):
filepathNorm = filepath.toLocalFile()
if not filepathNorm:
filepathNorm = filepath.toString()
else:
filepathNorm = QUrl(filepath).toLocalFile()
if not filepathNorm:
filepathNorm = QUrl.fromLocalFile(filepath).toLocalFile()
return filepathNorm

def _recentImportedImagesFolders(self):
folders = []
settings = QSettings()
Expand Down Expand Up @@ -786,10 +841,12 @@ def setDefaultSubmitter(self, name):
licensesModel = Property("QVariantList", _licensesModel, constant=True)
pipelineTemplateFilesChanged = Signal()
recentProjectFilesChanged = Signal()
recentTemplateFilesChanged = Signal()
recentImportedImagesFoldersChanged = Signal()
pipelineTemplateFiles = Property("QVariantList", _pipelineTemplateFiles, notify=pipelineTemplateFilesChanged)
pipelineTemplateNames = Property("QVariantList", _pipelineTemplateNames, notify=pipelineTemplateFilesChanged)
recentProjectFiles = Property("QVariantList", lambda self: self._recentProjectFiles, notify=recentProjectFilesChanged)
recentTemplateFiles = Property("QVariantList", lambda self: self._recentTemplateFiles, notify=recentTemplateFilesChanged)
recentImportedImagesFolders = Property("QVariantList", _recentImportedImagesFolders, notify=recentImportedImagesFoldersChanged)
default8bitViewerEnabled = Property(bool, _default8bitViewerEnabled, constant=True)
defaultSequencePlayerEnabled = Property(bool, _defaultSequencePlayerEnabled, constant=True)
Expand Down
Loading
Loading