diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index f98d6b127b..812dbf60f9 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -19,7 +19,7 @@ pass from meshroom.core.submitter import BaseSubmitter -from . import desc +from meshroom.core import desc # Setup logging logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index ab729d221f..6167f00ac3 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -717,9 +717,53 @@ class Node(object): documentation = '' category = 'Other' + _isPlugin = True + def __init__(self): super(Node, self).__init__() self.hasDynamicOutputAttribute = any(output.isDynamicValue for output in self.outputs) + try: + self.envFile + self.envType + except: + self._isPlugin=False + + @property + def envType(cls): + from core.plugin import EnvType #lazy import for plugin to avoid circular dependency + return EnvType.NONE + + @property + def envFile(cls): + """ + Env file used to build the environement, you may overwrite this to custom the behaviour + """ + raise NotImplementedError("You must specify an env file") + + @property + def _envName(cls): + """ + Get the env name by hashing the env files, overwrite this to use a custom pre-build env + """ + with open(cls.envFile, 'r') as file: + envContent = file.read() + from meshroom.core.plugin import getEnvName #lazy import as to avoid circular dep + return getEnvName(envContent) + + @property + def isPlugin(self): + """ + Tests if the node is a valid plugin node + """ + return self._isPlugin + + @property + def isBuilt(self): + """ + Tests if the environnement is built + """ + from meshroom.core.plugin import isBuilt + return self._isPlugin and isBuilt(self) def upgradeAttributeValues(self, attrValues, fromVersion): return attrValues @@ -790,7 +834,17 @@ def buildCommandLine(self, chunk): if chunk.node.isParallelized and chunk.node.size > 1: cmdSuffix = ' ' + self.commandLineRange.format(**chunk.range.toDict()) - return cmdPrefix + chunk.node.nodeDesc.commandLine.format(**chunk.node._cmdVars) + cmdSuffix + cmd=cmdPrefix + chunk.node.nodeDesc.commandLine.format(**chunk.node._cmdVars) + cmdSuffix + + #the process in Popen does not seem to use the right python, even if meshroom_compute is called within the env + #so in the case of command line using python, we have to make sure it is using the correct python + from meshroom.core.plugin import EnvType, getVenvPath, getVenvExe #lazy import to prevent circular dep + if self.isPlugin and self.envType == EnvType.VENV: + envPath = getVenvPath(self._envName) + envExe = getVenvExe(envPath) + cmd=cmd.replace("python", envExe) + + return cmd def stopProcess(self, chunk): # The same node could exists several times in the graph and diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 3cfd9bf508..83c3a40574 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -21,7 +21,6 @@ from meshroom.core.attribute import attributeFactory, ListAttribute, GroupAttribute, Attribute from meshroom.core.exception import NodeUpgradeError, UnknownNodeTypeError - def getWritingFilepath(filepath): return filepath + '.writing.' + str(uuid.uuid4()) @@ -406,13 +405,14 @@ def process(self, forceCompute=False): #if plugin node and if first call call meshroom_compute inside the env on 'host' so that the processchunk # of the node will be ran into the env - if hasattr(self.node.nodeDesc, 'envFile') and self._status.status!=Status.FIRST_RUN: + if self.node.nodeDesc.isPlugin and self._status.status!=Status.FIRST_RUN: try: - if not self.node.nodeDesc.isBuild(): + from meshroom.core.plugin import isBuilt, build, getCommandLine #lazy import to avoid circular dep + if not isBuilt(self.node.nodeDesc): self.upgradeStatusTo(Status.BUILD) - self.node.nodeDesc.build() + build(self.node.nodeDesc) self.upgradeStatusTo(Status.FIRST_RUN) - command = self.node.nodeDesc.getCommandLine(self) + command = getCommandLine(self) #NOTE: docker returns 0 even if mount fail (it fails on the deamon side) logging.info("Running plugin node with "+command) status = os.system(command) @@ -482,6 +482,7 @@ def isExtern(self): statusNodeName = Property(str, lambda self: self._status.nodeName, constant=True) elapsedTime = Property(float, lambda self: self._status.elapsedTime, notify=statusChanged) + # Simple structure for storing node position @@ -1413,6 +1414,8 @@ def has3DOutputAttribute(self): hasSequenceOutput = Property(bool, hasSequenceOutputAttribute, notify=outputAttrEnabledChanged) has3DOutput = Property(bool, has3DOutputAttribute, notify=outputAttrEnabledChanged) + isPlugin = Property(bool, lambda self: self.nodeDesc.isPlugin, constant=True) + isBuilt = Property(bool, lambda self: self.nodeDesc.isBuilt, constant=True) class Node(BaseNode): """ diff --git a/meshroom/core/plugin.py b/meshroom/core/plugin.py index c7db6c95f3..d54e97f0ed 100644 --- a/meshroom/core/plugin.py +++ b/meshroom/core/plugin.py @@ -17,10 +17,11 @@ import venv import inspect -from meshroom.core import desc, hashValue -from meshroom.core import pluginsNodesFolder, pluginsPipelinesFolder, defaultCacheFolder, pluginCatalogFile +from meshroom.core import desc +from meshroom.core import pluginsNodesFolder, pluginsPipelinesFolder, pluginCatalogFile, defaultCacheFolder from meshroom.core import meshroomFolder from meshroom.core.graph import loadGraph +from meshroom.core import hashValue #where the executables are (eg meshroom compute) meshroomBinDir = os.path.abspath(os.path.join(meshroomFolder, "..", "bin")) @@ -54,6 +55,9 @@ def __init__(self, pluginUrl, jsonData=None): if "pipelineFolder" in jsonData.keys(): self.pipelineFolder = os.path.join(pluginUrl, jsonData["pipelineFolder"]) +def getEnvName(envContent): + return "meshroom_plugin_"+hashValue(envContent) + def _dockerImageExists(image_name, tag='latest'): """ Check if the desired image:tag exists @@ -103,11 +107,14 @@ def getVenvExe(venvPath): raise FileNotFoundError(f"Python executable not found in the specified virtual environment: "+executable) return executable +def getVenvPath(envName): + return os.path.join(defaultCacheFolder, envName) + def _venvExists(envName): """ Check if the following virtual env exists """ - return os.path.isdir(os.path.join(defaultCacheFolder, envName)) + return os.path.isdir(getVenvPath(envName)) def installPlugin(pluginUrl): """ @@ -203,166 +210,141 @@ def uninstallPlugin(pluginUrl): os.unlink(pluginUrl) else: os.removedirs(pluginUrl) - -class PluginNode(desc.Node): + +def isBuilt(nodeDesc): """ - Class to be used to make a plugin node, you need to overwrite envType and envFile + Check if the env needs to be build for a specific nodesc. """ + if nodeDesc.envType == EnvType.NONE: + return True + elif nodeDesc.envType == EnvType.PIP: + #NOTE: could find way to check for installed packages instead of rebuilding all the time + return False + elif nodeDesc.envType == EnvType.VENV: + return _venvExists(nodeDesc._envName) + elif nodeDesc.envType == EnvType.CONDA: + return _condaEnvExist(nodeDesc._envName) + elif nodeDesc.envType == EnvType.DOCKER: + return _dockerImageExists(nodeDesc._envName) - @property - def envType(cls): - """ - Dynamic env type - """ - raise NotImplementedError("You must specify one or several envtype in the node description") - - @property - def envFile(cls): - """ - Env file used to build the environement, you may overwrite this to custom the behaviour - """ - raise NotImplementedError("You must specify an env file") +def build(nodeDesc): + """ + Perform the needed steps to prepare the environement in which to run the node. + """ + if not hasattr(nodeDesc, 'envFile'): + raise RuntimeError("The nodedesc has no env file") + returnValue = 0 + if nodeDesc.envType == EnvType.NONE: + pass + elif nodeDesc.envType == EnvType.PIP: + #install packages in the same python as meshroom + logging.info("Installing packages from "+ nodeDesc.envFile) + buildCommand = sys.executable+" -m pip install "+ nodeDesc.envFile + logging.info("Building with "+buildCommand+" ...") + returnValue = os.system(buildCommand) + logging.info("Done") + elif nodeDesc.envType == EnvType.VENV: + #create venv in default cache folder + envPath = getVenvPath(nodeDesc._envName) + logging.info("Creating virtual env "+envPath+" from "+nodeDesc.envFile) + venv.create(envPath, with_pip=True) + logging.info("Installing dependencies") + envExe = getVenvExe(envPath) + returnValue = os.system(_cleanEnvVarsRez()+envExe+" -m pip install -r "+ nodeDesc.envFile) + venvPythonLibFolder = os.path.join(os.path.dirname(envExe), '..', 'lib') + venvPythonLibFolder = [os.path.join(venvPythonLibFolder, p) + for p in os.listdir(venvPythonLibFolder) if p.startswith("python")][0] + os.symlink(meshroomFolder,os.path.join(venvPythonLibFolder, 'site-packages', 'meshroom')) + logging.info("Done") + elif nodeDesc.envType == EnvType.CONDA: + #build a conda env from a yaml file + logging.info("Creating conda env "+nodeDesc._envName+" from "+nodeDesc.envFile) + makeEnvCommand = ( _cleanEnvVarsRez()+" conda config --set channel_priority strict ; " + +" conda env create -v -v --name "+nodeDesc._envName + +" --file "+nodeDesc.envFile+" ") + logging.info("Making conda env") + logging.info(makeEnvCommand) + returnValue = os.system(makeEnvCommand) + #find path to env's folder and add symlink to meshroom + condaPythonExecudable=subprocess.check_output(_cleanEnvVarsRez()+"conda run -n "+nodeDesc._envName + +" python -c \"import sys; print(sys.executable)\"", + shell=True).strip().decode('UTF-8') + condaPythonLibFolder=os.path.join(os.path.dirname(condaPythonExecudable), '..', 'lib') + condaPythonLibFolder=[os.path.join(condaPythonLibFolder, p) + for p in os.listdir(condaPythonLibFolder) if p.startswith("python")][0] + os.symlink(meshroomFolder,os.path.join(condaPythonLibFolder, 'meshroom')) + logging.info("Done making conda env") + elif nodeDesc.envType == EnvType.DOCKER: + #build docker image + logging.info("Creating image "+nodeDesc._envName+" from "+ nodeDesc.envFile) + buildCommand = "docker build -f "+nodeDesc.envFile+" -t "+nodeDesc._envName+" "+os.path.dirname(nodeDesc.envFile) + logging.info("Building with "+buildCommand+" ...") + returnValue = os.system(buildCommand) + logging.info("Done") + else: + raise RuntimeError("Invalid env type") + if returnValue != 0: + raise RuntimeError("Something went wrong during build") - @property - def _envName(cls): - """ - Get the env name by hashing the env files, overwrite this to use a custom pre-build env - """ - with open(cls.envFile, 'r') as file: - envContent = file.read() - return "meshroom_plugin_"+hashValue(envContent) +def getCommandLine(chunk): + """ + Return the command line needed to enter the environment + meshroom_compute + Will make meshroom available in the environment. + """ + nodeDesc=chunk.node.nodeDesc + if chunk.node.isParallelized: + raise RuntimeError("Parallelisation not supported for plugin nodes") + if chunk.node.graph.filepath == "": + raise RuntimeError("The project needs to be saved to use plugin nodes") + saved_graph = loadGraph(chunk.node.graph.filepath) + if (str(chunk.node) not in [str(f) for f in saved_graph._nodes._objects] + or chunk.node._uids[0] != saved_graph.findNode(str(chunk.node))._uids[0] ): + raise RuntimeError("The changes needs to be saved to use plugin nodes") - def isBuild(cls): - """ - Check if the env needs to be build. - """ - if cls.envType == EnvType.NONE: - return True - elif cls.envType == EnvType.PIP: - #NOTE: could find way to check for installed packages instead of rebuilding all the time - return False - elif cls.envType == EnvType.VENV: - return _venvExists(cls._envName) - elif cls.envType == EnvType.CONDA: - return _condaEnvExist(cls._envName) - elif cls.envType == EnvType.DOCKER: - return _dockerImageExists(cls._envName) + cmdPrefix = "" + # vars common to venv and conda, that will be passed when runing conda run or venv + meshroomCompute= meshroomBinDir+"/meshroom_compute" + meshroomComputeArgs = "--node "+chunk.node.name+" \""+chunk.node.graph.filepath+"\"" + pythonsetMeshroomPath = "export PYTHONPATH="+meshroomFolder+":$PYTHONPATH;" - def build(cls): - """ - Perform the needed steps to prepare the environement in which to run the node. - """ - if cls.envType == EnvType.NONE: - pass - elif cls.envType == EnvType.PIP: - #install packages in the same python as meshroom - logging.info("Installing packages from "+ cls.envFile) - buildCommand = sys.executable+" -m pip install "+ cls.envFile - logging.info("Building with "+buildCommand+" ...") - returnValue = os.system(buildCommand) - logging.info("Done") - elif cls.envType == EnvType.VENV: - #create venv in default cache folder - logging.info("Creating virtual env "+os.path.join(defaultCacheFolder, cls._envName)+" from "+cls.envFile) - envPath = os.path.join(defaultCacheFolder, cls._envName) - venv.create(envPath, with_pip=True) - logging.info("Installing dependencies") - envExe = getVenvExe(envPath) - returnValue = os.system(_cleanEnvVarsRez()+envExe+" -m pip install -r "+ cls.envFile) - venvPythonLibFolder = os.path.join(os.path.dirname(envExe), '..', 'lib') - venvPythonLibFolder = [os.path.join(venvPythonLibFolder, p) - for p in os.listdir(venvPythonLibFolder) if p.startswith("python")][0] - os.symlink(meshroomFolder,os.path.join(venvPythonLibFolder, 'site-packages', 'meshroom')) - logging.info("Done") - elif cls.envType == EnvType.CONDA: - #build a conda env from a yaml file - logging.info("Creating conda env "+cls._envName+" from "+cls.envFile) - makeEnvCommand = ( _cleanEnvVarsRez()+" conda config --set channel_priority strict ; " - +" conda env create -v -v --name "+cls._envName - +" --file "+cls.envFile+" ") - logging.info("Making conda env") - logging.info(makeEnvCommand) - returnValue = os.system(makeEnvCommand) - #find path to env's folder and add symlink to meshroom - condaPythonExecudable=subprocess.check_output(_cleanEnvVarsRez()+"conda run -n "+cls._envName - +" python -c \"import sys; print(sys.executable)\"", - shell=True).strip().decode('UTF-8') - condaPythonLibFolder=os.path.join(os.path.dirname(condaPythonExecudable), '..', 'lib') - condaPythonLibFolder=[os.path.join(condaPythonLibFolder, p) - for p in os.listdir(condaPythonLibFolder) if p.startswith("python")][0] - os.symlink(meshroomFolder,os.path.join(condaPythonLibFolder, 'meshroom')) - logging.info("Done making conda env") - elif cls.envType == EnvType.DOCKER: - #build docker image - logging.info("Creating image "+cls._envName+" from "+ cls.envFile) - buildCommand = "docker build -f "+cls.envFile+" -t "+cls._envName+" "+os.path.dirname(cls.envFile) - logging.info("Building with "+buildCommand+" ...") - returnValue = os.system(buildCommand) - logging.info("Done") - if returnValue != 0: - raise RuntimeError("Something went wrong during build") - - def getCommandLine(cls, chunk): - """ - Return the command line needed to enter the environment + meshroom_compute - Will make meshroom available in the environment. - """ - if chunk.node.isParallelized: - raise RuntimeError("Parallelisation not supported for plugin nodes") - if chunk.node.graph.filepath == "": - raise RuntimeError("The project needs to be saved to use plugin nodes") - saved_graph = loadGraph(chunk.node.graph.filepath) - if (str(chunk.node) not in [str(f) for f in saved_graph._nodes._objects] - or chunk.node._uids[0] != saved_graph.findNode(str(chunk.node))._uids[0] ): - raise RuntimeError("The changes needs to be saved to use plugin nodes") - - cmdPrefix = "" - # vars common to venv and conda, that will be passed when runing conda run or venv - meshroomCompute= meshroomBinDir+"/meshroom_compute" - meshroomComputeArgs = "--node "+chunk.node.name+" \""+chunk.node.graph.filepath+"\"" - pythonsetMeshroomPath = "export PYTHONPATH="+meshroomFolder+":$PYTHONPATH;" + if nodeDesc.envType == EnvType.VENV: + envPath = getVenvPath(nodeDesc._envName) + envExe = getVenvExe(envPath) + #make sure meshroom in in pythonpath and that we call the right python + cmdPrefix = _cleanEnvVarsRez()+pythonsetMeshroomPath+" "+envExe + " "+ meshroomCompute +" " + elif nodeDesc.envType == EnvType.CONDA: + #NOTE: system env vars are not passed to conda run, we installed it 'manually' before + cmdPrefix = _cleanEnvVarsRez()+" conda run --cwd "+os.path.join(meshroomFolder, "..")\ + +" --no-capture-output -n "+nodeDesc._envName+" "+" python "+meshroomCompute + elif nodeDesc.envType == EnvType.DOCKER: + #path to the selected plugin + classFile=inspect.getfile(chunk.node.nodeDesc.__class__) + pluginDir = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(classFile),".."))) + #path to the project/cache + projectDir=os.path.abspath(os.path.realpath(os.path.dirname(chunk.node.graph.filepath))) + mountCommand = (' --mount type=bind,source="'+projectDir+'",target="'+projectDir+'"' #mount with same file hierarchy + +' --mount type=bind,source="'+pluginDir+'",target=/meshroomPlugin,readonly' #mount the plugin folder (because of symbolic link, not necesseraly physically in meshroom's folder) + +' --mount type=bind,source="'+meshroomFolder+'",target=/meshroomFolder/meshroom,readonly' + +' --mount type=bind,source="'+meshroomBinDir+'",target=/meshroomBinDir,readonly') + #adds meshroom's code(& the plugin actual path that can be different (because of the dymbolic link)) to the python path + envCommand = " --env PYTHONPATH=/meshroomFolder --env MESHROOM_NODES_PATH=/meshroomPlugin" + #adds the gpu arg if needed + runtimeArg="" + if chunk.node.nodeDesc.gpu != desc.Level.NONE: + runtimeArg="--runtime=nvidia --gpus all" + #compose cl + cmdPrefix = "docker run -it --rm "+runtimeArg+" "+mountCommand+" "+envCommand+" "+nodeDesc._envName +" \"python /meshroomBinDir/meshroom_compute " + meshroomComputeArgs="--node "+chunk.node.name+" "+chunk.node.graph.filepath+"\"" + else: + raise RuntimeError("NodeType not recognised") - if cls.envType == EnvType.VENV: - envPath = os.path.join(defaultCacheFolder, cls._envName) - envExe = getVenvExe(envPath) - #make sure meshroom in in pythonpath and that we call the right python - cmdPrefix = _cleanEnvVarsRez()+pythonsetMeshroomPath+" "+envExe + " "+ meshroomCompute +" " - elif cls.envType == EnvType.CONDA: - #NOTE: system env vars are not passed to conda run, we installed it 'manually' before - cmdPrefix = _cleanEnvVarsRez()+" conda run --cwd "+os.path.join(meshroomFolder, "..")\ - +" --no-capture-output -n "+cls._envName+" "+" python "+meshroomCompute - elif cls.envType == EnvType.DOCKER: - #path to the selected plugin - classFile=inspect.getfile(chunk.node.nodeDesc.__class__) - pluginDir = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(classFile),".."))) - #path to the project/cache - projectDir=os.path.abspath(os.path.realpath(os.path.dirname(chunk.node.graph.filepath))) - mountCommand = (' --mount type=bind,source="'+projectDir+'",target="'+projectDir+'"' #mount with same file hierarchy - +' --mount type=bind,source="'+pluginDir+'",target=/meshroomPlugin,readonly' #mount the plugin folder (because of symbolic link, not necesseraly physically in meshroom's folder) - +' --mount type=bind,source="'+meshroomFolder+'",target=/meshroomFolder/meshroom,readonly' - +' --mount type=bind,source="'+meshroomBinDir+'",target=/meshroomBinDir,readonly') - #adds meshroom's code(& the plugin actual path that can be different (because of the dymbolic link)) to the python path - envCommand = " --env PYTHONPATH=/meshroomFolder --env MESHROOM_NODES_PATH=/meshroomPlugin" - #adds the gpu arg if needed - runtimeArg="" - if cls.gpu != desc.Level.NONE: - runtimeArg="--runtime=nvidia --gpus all" - #compose cl - cmdPrefix = "docker run -it --rm "+runtimeArg+" "+mountCommand+" "+envCommand+" "+cls._envName +" \"python /meshroomBinDir/meshroom_compute " - meshroomComputeArgs="--node "+chunk.node.name+" "+chunk.node.graph.filepath+"\"" + command=cmdPrefix+" "+meshroomComputeArgs + + return command - command=cmdPrefix+" "+meshroomComputeArgs - - return command +# you may use these to esplicitly define Pluginnodes +class PluginNode(desc.Node): + pass -#class that call command line nodes in an env -class PluginCommandLineNode(PluginNode, desc.CommandLineNode): - def buildCommandLine(self, chunk): - cmd = super().buildCommandLine(chunk) - #the process in Popen does not seem to use the right python, even if meshroom_compute is call within the env - #so in the case of command line using python, we have to make sur it is using the correct python - if self.envType == EnvType.VENV: - envPath = os.path.join(defaultCacheFolder, self._envName) - envExe = getVenvExe(envPath) - cmd=cmd.replace("python", envExe) - return cmd +class PluginCommandLineNode(desc.CommandLineNode): + pass \ No newline at end of file diff --git a/meshroom/plugins/catalog.json b/meshroom/plugins/catalog.json index 556ac56429..f4609c0c9d 100644 --- a/meshroom/plugins/catalog.json +++ b/meshroom/plugins/catalog.json @@ -2,8 +2,8 @@ { "pluginName":"Meshroom Research", "pluginUrl":"https://github.com/alicevision/MeshroomResearch/", - "description":"Meshroom-Research comprises a collection of plugins for Meshroom, mostly develloped in-house at MikrosImage", + "description":"Meshroom-Research comprises a collection of experimental plugins for Meshroom", "isCollection":true, "nodeTypes":["Python", "Docker", "Conda"] } -] \ No newline at end of file +] diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml index 02e02e48fa..18cee26d31 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -131,6 +131,73 @@ Page { } } + //File browser for plugin + Dialog { + id: pluginURLDialog + title: "Plugin URL" + height: 150 + width: 300 + standardButtons: StandardButton.Ok | StandardButton.Cancel + //focus: true + Column { + anchors.fill: parent + Text { + text: "Plugin URL" + height: 40 + } + TextField { + id: urlInput + width: parent.width * 0.75 + focus: true + } + } + onButtonClicked: { + if (clickedButton==StandardButton.Ok) { + console.log("Accepted " + clickedButton) + if (_reconstruction.installPlugin(urlInput.text)) { + pluginInstalledDialog.open() + } else { + pluginNotInstalledDialog.open() + } + } + } + } + + // dialogs for plugins + MessageDialog { + id: pluginInstalledDialog + title: "Plugin installed" + modal: true + canCopy: false + Label { + text: "Plugin installed, please restart meshroom for the changes to take effect" + } + } + + MessageDialog { + id: pluginNotInstalledDialog + title: "Plugin not installed" + modal: true + canCopy: false + Label { + text: "Something went wrong, plugin not installed" + } + } + + // plugin installation from path or url + Platform.FolderDialog { + id: intallPluginDialog + options: Platform.FolderDialog.DontUseNativeDialog + title: "Install Plugin" + onAccepted: { + if (_reconstruction.installPlugin(currentFolder.toString())) { + pluginInstalledDialog.open() + } else { + pluginNotInstalledDialog.open() + } + } + } + Item { id: computeManager @@ -525,6 +592,23 @@ Page { } } + Action { + id: installPluginFromFolderAction + text: "Install Plugin From Local Folder" + onTriggered: { + initFileDialogFolder(intallPluginDialog) + intallPluginDialog.open() + } + } + + Action { + id: installPluginFromURLAction + text: "Install Plugin From URL" + onTriggered: { + pluginURLDialog.open() + } + } + header: RowLayout { spacing: 0 MaterialToolButton { @@ -1204,6 +1288,18 @@ Page { var n = _reconstruction.upgradeNode(node) _reconstruction.selectedNode = n } + + onDoBuild: { + try { + _reconstruction.buildNode(node.name) + node.isNotBuilt=false + } catch (error) { + //NOTE: could do an error popup + console.log("Build error:") + console.log(error) + } + + } } } } diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 9a8bb05a17..4de416c420 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -19,6 +19,9 @@ Item { property bool readOnly: node.locked /// Whether the node is in compatibility mode readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false + /// Whether the node is a plugin that needs to be build + readonly property bool isPlugin: node ? node.isPlugin : false + property bool isNotBuilt: node ? (!node.isBuilt) : false /// Mouse related states property bool mainSelected: false property bool selected: false @@ -28,7 +31,8 @@ Item { property point position: Qt.point(x, y) /// Styling property color shadowColor: "#cc000000" - readonly property color defaultColor: isCompatibilityNode ? "#444" : !node.isComputable ? "#BA3D69" : activePalette.base + //readonly property color defaultColor: isCompatibilityNode ? "#444" : ((isPlugin && isNotBuilt) ? "#444": (!node.isComputable ? "#BA3D69" : activePalette.base)) + readonly property color defaultColor: isCompatibilityNode ? "#444" : (!node.isComputable ? "#BA3D69" : activePalette.base) property color baseColor: defaultColor property point mousePosition: Qt.point(mouseArea.mouseX, mouseArea.mouseY) @@ -232,6 +236,15 @@ Item { issueDetails: root.node.issueDetails } } + + // ToBuild icon for PluginNodes + Loader { + active: root.isPlugin && root.isNotBuilt + sourceComponent: ToBuildBadge { + sourceComponent: iconDelegate + } + } + // Data sharing indicator // Note: for an unknown reason, there are some performance issues with the UI refresh. diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 3d4684229e..1e8a58177d 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -19,9 +19,12 @@ Panel { property bool readOnly: false property bool isCompatibilityNode: node && node.compatibilityIssue !== undefined property string nodeStartDateTime: "" + readonly property bool isPlugin: node ? node.isPlugin : false + readonly property bool isNotBuilt: node ? (!node.isBuilt) : false signal attributeDoubleClicked(var mouse, var attribute) signal upgradeRequest() + signal doBuild() title: "Node" + (node !== null ? " - " + node.label + "" + (node.label !== node.defaultLabel ? " (" + node.defaultLabel + ")" : "") : "") icon: MaterialLabel { text: MaterialIcons.tune } @@ -226,6 +229,17 @@ Panel { } } + Loader { + active: root.isPlugin && root.isNotBuilt + Layout.fillWidth: true + visible: active // for layout update + + sourceComponent: ToBuildBadge { + onDoBuild: root.doBuild() + sourceComponent: bannerDelegate + } + } + Loader { Layout.fillHeight: true Layout.fillWidth: true diff --git a/meshroom/ui/qml/GraphEditor/ToBuildBadge.qml b/meshroom/ui/qml/GraphEditor/ToBuildBadge.qml new file mode 100644 index 0000000000..f7333700f0 --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/ToBuildBadge.qml @@ -0,0 +1,66 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.11 +import MaterialIcons 2.2 + +Loader { + id: root + + sourceComponent: iconDelegate + + signal doBuild() + + property Component iconDelegate: Component { + + Label { + text: MaterialIcons.warning + font.family: MaterialIcons.fontFamily + font.pointSize: 12 + color: "#66207F" + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPressed: mouse.accepted = false + ToolTip.text: "Node env needs to be built" + ToolTip.visible: containsMouse + } + } + } + + property Component bannerDelegate: Component { + + Pane { + padding: 6 + clip: true + background: Rectangle { color: "#66207F" } + + RowLayout { + width: parent.width + Column { + Layout.fillWidth: true + Label { + width: parent.width + elide: Label.ElideMiddle + font.bold: true + text: "Env needs to be built" + color: "white" + } + Label { + width: parent.width + elide: Label.ElideMiddle + color: "white" + } + } + Button { + visible: (parent.width > width) ? 1 : 0 + palette.window: root.color + palette.button: Qt.darker(root.color, 1.2) + palette.buttonText: "white" + text: "Build" + onClicked: doBuild() + } + } + } + } +} diff --git a/meshroom/ui/qml/GraphEditor/qmldir b/meshroom/ui/qml/GraphEditor/qmldir index 4a4d4ca460..dccf2d5ba9 100644 --- a/meshroom/ui/qml/GraphEditor/qmldir +++ b/meshroom/ui/qml/GraphEditor/qmldir @@ -9,7 +9,8 @@ AttributePin 1.0 AttributePin.qml AttributeEditor 1.0 AttributeEditor.qml AttributeItemDelegate 1.0 AttributeItemDelegate.qml CompatibilityBadge 1.0 CompatibilityBadge.qml +ToBuildBadge 1.0 ToBuildBadge.qml CompatibilityManager 1.0 CompatibilityManager.qml singleton GraphEditorSettings 1.0 GraphEditorSettings.qml TaskManager 1.0 TaskManager.qml -ScriptEditor 1.0 ScriptEditor.qml \ No newline at end of file +ScriptEditor 1.0 ScriptEditor.qml diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 5832af07d8..f011326450 100644 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -132,73 +132,6 @@ ApplicationWindow { } } - //File browser for plugin - Dialog { - id: pluginURLDialog - title: "Plugin URL" - height: 150 - width: 300 - standardButtons: StandardButton.Ok | StandardButton.Cancel - //focus: true - Column { - anchors.fill: parent - Text { - text: "Plugin URL" - height: 40 - } - TextField { - id: urlInput - width: parent.width * 0.75 - focus: true - } - } - onButtonClicked: { - if (clickedButton==StandardButton.Ok) { - console.log("Accepted " + clickedButton) - if (_reconstruction.installPlugin(urlInput.text)) { - pluginInstalledDialog.open() - } else { - pluginNotInstalledDialog.open() - } - } - } - } - - // dialogs for plugins - MessageDialog { - id: pluginInstalledDialog - title: "Plugin installed" - modal: true - canCopy: false - Label { - text: "Plugin installed, please restart meshroom for the changes to take effect" - } - } - - MessageDialog { - id: pluginNotInstalledDialog - title: "Plugin not installed" - modal: true - canCopy: false - Label { - text: "Something went wrong, plugin not installed" - } - } - - // plugin installation from path or url - Platform.FolderDialog { - id: intallPluginDialog - options: Platform.FolderDialog.DontUseNativeDialog - title: "Install Plugin" - onAccepted: { - if (_reconstruction.installPlugin(currentFolder.toString())) { - pluginInstalledDialog.open() - } else { - pluginNotInstalledDialog.open() - } - } - } - // Check if document has been saved function ensureSaved(callback) { diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 225ded9bae..8918d22ac6 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -586,6 +586,14 @@ def installPlugin(self, url): localFile = prepareUrlLocalFile(url) return installPlugin(localFile) + @Slot(str, result=bool) + def buildNode(self, nodeName): + print("***Building "+nodeName) + node = self._graph.node(nodeName) + from meshroom.core.plugin import isBuilt, build #lazy import to avoid circular dep + if not isBuilt(node.nodeDesc): + build(node.nodeDesc) + def onGraphChanged(self): """ React to the change of the internal graph. """ self._liveSfmManager.reset() diff --git a/tests/test_plugin_nodes.py b/tests/test_plugin_nodes.py index 1df862553d..cc44461a0c 100644 --- a/tests/test_plugin_nodes.py +++ b/tests/test_plugin_nodes.py @@ -6,6 +6,7 @@ logging = logging.getLogger(__name__) def test_pluginNodes(): + #Dont run the tests in the CI as we are unable to install plugins beforehand if "CI" in os.environ: return graph = Graph('') @@ -14,4 +15,4 @@ def test_pluginNodes(): graph.addNewNode('DummyPipNode') graph.addNewNode('DummyVenvNode') - \ No newline at end of file +