mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-06 10:18:42 +02:00
moved plugin logic to nodeDesc instead of class
added build badge changes for uid renaming added back menu after update fix for existing symlink
This commit is contained in:
parent
c2aac17c88
commit
370d1346b4
13 changed files with 429 additions and 244 deletions
|
@ -19,7 +19,7 @@ except Exception:
|
|||
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)
|
||||
|
|
|
@ -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
|
||||
|
@ -806,7 +850,17 @@ class CommandLineNode(Node):
|
|||
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
|
||||
|
|
|
@ -21,7 +21,6 @@ from meshroom.core import desc, stats, hashValue, nodeVersion, Version
|
|||
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 @@ class NodeChunk(BaseObject):
|
|||
|
||||
#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)
|
||||
|
@ -484,6 +484,7 @@ class NodeChunk(BaseObject):
|
|||
elapsedTime = Property(float, lambda self: self._status.elapsedTime, notify=statusChanged)
|
||||
|
||||
|
||||
|
||||
# Simple structure for storing node position
|
||||
Position = namedtuple("Position", ["x", "y"])
|
||||
# Initialize default coordinates values to 0
|
||||
|
@ -1416,6 +1417,8 @@ class BaseNode(BaseObject):
|
|||
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):
|
||||
"""
|
||||
|
|
|
@ -17,10 +17,11 @@ import subprocess
|
|||
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 @@ class PluginParams():
|
|||
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):
|
||||
"""
|
||||
|
@ -163,7 +170,11 @@ def installPlugin(pluginUrl):
|
|||
if isLocal:
|
||||
os.symlink(pluginParam.nodesFolder, intallFolder)
|
||||
if os.path.isdir(pluginParam.pipelineFolder):
|
||||
os.symlink(pluginParam.pipelineFolder, os.path.join(pluginsPipelinesFolder, pluginParam.pluginName))
|
||||
pipelineFolderLink = os.path.join(pluginsPipelinesFolder, pluginParam.pluginName)
|
||||
if os.path.exists(pipelineFolderLink):
|
||||
logging.warn("Plugin already installed, will overwrite")
|
||||
os.unlink(pipelineFolderLink)
|
||||
os.symlink(pluginParam.pipelineFolder, pipelineFolderLink)
|
||||
else:
|
||||
copy_tree(pluginParam.nodesFolder, intallFolder)
|
||||
if os.path.isdir(pluginParam.pipelineFolder):
|
||||
|
@ -204,87 +215,62 @@ def uninstallPlugin(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.
|
||||
"""
|
||||
|
||||
@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")
|
||||
|
||||
@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 isBuild(cls):
|
||||
"""
|
||||
Check if the env needs to be build.
|
||||
"""
|
||||
if cls.envType == EnvType.NONE:
|
||||
if nodeDesc.envType == EnvType.NONE:
|
||||
return True
|
||||
elif cls.envType == EnvType.PIP:
|
||||
elif nodeDesc.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)
|
||||
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)
|
||||
|
||||
def build(cls):
|
||||
def build(nodeDesc):
|
||||
"""
|
||||
Perform the needed steps to prepare the environement in which to run the node.
|
||||
"""
|
||||
if cls.envType == EnvType.NONE:
|
||||
if not hasattr(nodeDesc, 'envFile'):
|
||||
raise RuntimeError("The nodedesc has no env file")
|
||||
returnValue = 0
|
||||
if nodeDesc.envType == EnvType.NONE:
|
||||
pass
|
||||
elif cls.envType == EnvType.PIP:
|
||||
elif nodeDesc.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("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 cls.envType == EnvType.VENV:
|
||||
elif nodeDesc.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)
|
||||
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 "+ cls.envFile)
|
||||
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 cls.envType == EnvType.CONDA:
|
||||
elif nodeDesc.envType == EnvType.CONDA:
|
||||
#build a conda env from a yaml file
|
||||
logging.info("Creating conda env "+cls._envName+" from "+cls.envFile)
|
||||
logging.info("Creating conda env "+nodeDesc._envName+" from "+nodeDesc.envFile)
|
||||
makeEnvCommand = ( _cleanEnvVarsRez()+" conda config --set channel_priority strict ; "
|
||||
+" conda env create -v -v --name "+cls._envName
|
||||
+" --file "+cls.envFile+" ")
|
||||
+" 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 "+cls._envName
|
||||
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')
|
||||
|
@ -292,28 +278,31 @@ class PluginNode(desc.Node):
|
|||
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:
|
||||
elif nodeDesc.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("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")
|
||||
|
||||
def getCommandLine(cls, chunk):
|
||||
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] ):
|
||||
or chunk.node._uid != saved_graph.findNode(str(chunk.node))._uid ):
|
||||
raise RuntimeError("The changes needs to be saved to use plugin nodes")
|
||||
|
||||
cmdPrefix = ""
|
||||
|
@ -322,16 +311,16 @@ class PluginNode(desc.Node):
|
|||
meshroomComputeArgs = "--node "+chunk.node.name+" \""+chunk.node.graph.filepath+"\""
|
||||
pythonsetMeshroomPath = "export PYTHONPATH="+meshroomFolder+":$PYTHONPATH;"
|
||||
|
||||
if cls.envType == EnvType.VENV:
|
||||
envPath = os.path.join(defaultCacheFolder, cls._envName)
|
||||
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 cls.envType == EnvType.CONDA:
|
||||
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 "+cls._envName+" "+" python "+meshroomCompute
|
||||
elif cls.envType == EnvType.DOCKER:
|
||||
+" --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),"..")))
|
||||
|
@ -345,24 +334,21 @@ class PluginNode(desc.Node):
|
|||
envCommand = " --env PYTHONPATH=/meshroomFolder --env MESHROOM_NODES_PATH=/meshroomPlugin"
|
||||
#adds the gpu arg if needed
|
||||
runtimeArg=""
|
||||
if cls.gpu != desc.Level.NONE:
|
||||
if chunk.node.nodeDesc.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 "
|
||||
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")
|
||||
|
||||
command=cmdPrefix+" "+meshroomComputeArgs
|
||||
|
||||
return command
|
||||
|
||||
#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
|
||||
# you may use these to explicitly define Pluginnodes
|
||||
class PluginNode(desc.Node):
|
||||
pass
|
||||
|
||||
class PluginCommandLineNode(desc.CommandLineNode):
|
||||
pass
|
|
@ -2,7 +2,7 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -741,6 +825,18 @@ Page {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: removeImagesFromAllGroupsAction.tooltip
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
action: installPluginFromFolderAction
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Install plugin from a folder"
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
action: installPluginFromURLAction
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Install plugin from a local or online url"
|
||||
}
|
||||
}
|
||||
MenuSeparator { }
|
||||
Action {
|
||||
|
@ -1214,6 +1310,17 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 @@ 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" : (!node.isComputable ? "#BA3D69" : activePalette.base)
|
||||
property color baseColor: defaultColor
|
||||
|
||||
property point mousePosition: Qt.point(mouseArea.mouseX, mouseArea.mouseY)
|
||||
|
@ -233,6 +236,15 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Example: a node duplicated 40 times will be slow while creating another identical node
|
||||
|
|
|
@ -18,9 +18,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 ? " - <b>" + node.label + "</b>" + (node.label !== node.defaultLabel ? " (" + node.defaultLabel + ")" : "") : "")
|
||||
icon: MaterialLabel { text: MaterialIcons.tune }
|
||||
|
@ -225,6 +228,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
|
||||
|
|
66
meshroom/ui/qml/GraphEditor/ToBuildBadge.qml
Normal file
66
meshroom/ui/qml/GraphEditor/ToBuildBadge.qml
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ 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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -586,6 +586,14 @@ class Reconstruction(UIGraph):
|
|||
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()
|
||||
|
|
|
@ -6,6 +6,7 @@ from meshroom.core.graph import Graph
|
|||
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('')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue