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:
Matthieu Hog 2024-09-02 16:49:58 +02:00
parent c2aac17c88
commit 370d1346b4
13 changed files with 429 additions and 244 deletions

View file

@ -19,7 +19,7 @@ except Exception:
pass pass
from meshroom.core.submitter import BaseSubmitter from meshroom.core.submitter import BaseSubmitter
from . import desc from meshroom.core import desc
# Setup logging # Setup logging
logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO) logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO)

View file

@ -717,9 +717,53 @@ class Node(object):
documentation = '' documentation = ''
category = 'Other' category = 'Other'
_isPlugin = True
def __init__(self): def __init__(self):
super(Node, self).__init__() super(Node, self).__init__()
self.hasDynamicOutputAttribute = any(output.isDynamicValue for output in self.outputs) 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): def upgradeAttributeValues(self, attrValues, fromVersion):
return attrValues return attrValues
@ -806,7 +850,17 @@ class CommandLineNode(Node):
if chunk.node.isParallelized and chunk.node.size > 1: if chunk.node.isParallelized and chunk.node.size > 1:
cmdSuffix = ' ' + self.commandLineRange.format(**chunk.range.toDict()) 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): def stopProcess(self, chunk):
# The same node could exists several times in the graph and # The same node could exists several times in the graph and

View file

@ -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.attribute import attributeFactory, ListAttribute, GroupAttribute, Attribute
from meshroom.core.exception import NodeUpgradeError, UnknownNodeTypeError from meshroom.core.exception import NodeUpgradeError, UnknownNodeTypeError
def getWritingFilepath(filepath): def getWritingFilepath(filepath):
return filepath + '.writing.' + str(uuid.uuid4()) 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 #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 # 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: 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.upgradeStatusTo(Status.BUILD)
self.node.nodeDesc.build() build(self.node.nodeDesc)
self.upgradeStatusTo(Status.FIRST_RUN) 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) #NOTE: docker returns 0 even if mount fail (it fails on the deamon side)
logging.info("Running plugin node with "+command) logging.info("Running plugin node with "+command)
status = os.system(command) status = os.system(command)
@ -484,6 +484,7 @@ class NodeChunk(BaseObject):
elapsedTime = Property(float, lambda self: self._status.elapsedTime, notify=statusChanged) elapsedTime = Property(float, lambda self: self._status.elapsedTime, notify=statusChanged)
# Simple structure for storing node position # Simple structure for storing node position
Position = namedtuple("Position", ["x", "y"]) Position = namedtuple("Position", ["x", "y"])
# Initialize default coordinates values to 0 # Initialize default coordinates values to 0
@ -1416,6 +1417,8 @@ class BaseNode(BaseObject):
hasSequenceOutput = Property(bool, hasSequenceOutputAttribute, notify=outputAttrEnabledChanged) hasSequenceOutput = Property(bool, hasSequenceOutputAttribute, notify=outputAttrEnabledChanged)
has3DOutput = Property(bool, has3DOutputAttribute, 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): class Node(BaseNode):
""" """

View file

@ -17,10 +17,11 @@ import subprocess
import venv import venv
import inspect import inspect
from meshroom.core import desc, hashValue from meshroom.core import desc
from meshroom.core import pluginsNodesFolder, pluginsPipelinesFolder, defaultCacheFolder, pluginCatalogFile from meshroom.core import pluginsNodesFolder, pluginsPipelinesFolder, pluginCatalogFile, defaultCacheFolder
from meshroom.core import meshroomFolder from meshroom.core import meshroomFolder
from meshroom.core.graph import loadGraph from meshroom.core.graph import loadGraph
from meshroom.core import hashValue
#where the executables are (eg meshroom compute) #where the executables are (eg meshroom compute)
meshroomBinDir = os.path.abspath(os.path.join(meshroomFolder, "..", "bin")) meshroomBinDir = os.path.abspath(os.path.join(meshroomFolder, "..", "bin"))
@ -54,6 +55,9 @@ class PluginParams():
if "pipelineFolder" in jsonData.keys(): if "pipelineFolder" in jsonData.keys():
self.pipelineFolder = os.path.join(pluginUrl, jsonData["pipelineFolder"]) self.pipelineFolder = os.path.join(pluginUrl, jsonData["pipelineFolder"])
def getEnvName(envContent):
return "meshroom_plugin_"+hashValue(envContent)
def _dockerImageExists(image_name, tag='latest'): def _dockerImageExists(image_name, tag='latest'):
""" """
Check if the desired image:tag exists 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) raise FileNotFoundError(f"Python executable not found in the specified virtual environment: "+executable)
return executable return executable
def getVenvPath(envName):
return os.path.join(defaultCacheFolder, envName)
def _venvExists(envName): def _venvExists(envName):
""" """
Check if the following virtual env exists 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): def installPlugin(pluginUrl):
""" """
@ -163,7 +170,11 @@ def installPlugin(pluginUrl):
if isLocal: if isLocal:
os.symlink(pluginParam.nodesFolder, intallFolder) os.symlink(pluginParam.nodesFolder, intallFolder)
if os.path.isdir(pluginParam.pipelineFolder): 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: else:
copy_tree(pluginParam.nodesFolder, intallFolder) copy_tree(pluginParam.nodesFolder, intallFolder)
if os.path.isdir(pluginParam.pipelineFolder): if os.path.isdir(pluginParam.pipelineFolder):
@ -204,87 +215,62 @@ def uninstallPlugin(pluginUrl):
else: else:
os.removedirs(pluginUrl) 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:
@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:
return True 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 #NOTE: could find way to check for installed packages instead of rebuilding all the time
return False return False
elif cls.envType == EnvType.VENV: elif nodeDesc.envType == EnvType.VENV:
return _venvExists(cls._envName) return _venvExists(nodeDesc._envName)
elif cls.envType == EnvType.CONDA: elif nodeDesc.envType == EnvType.CONDA:
return _condaEnvExist(cls._envName) return _condaEnvExist(nodeDesc._envName)
elif cls.envType == EnvType.DOCKER: elif nodeDesc.envType == EnvType.DOCKER:
return _dockerImageExists(cls._envName) return _dockerImageExists(nodeDesc._envName)
def build(cls): def build(nodeDesc):
""" """
Perform the needed steps to prepare the environement in which to run the node. 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 pass
elif cls.envType == EnvType.PIP: elif nodeDesc.envType == EnvType.PIP:
#install packages in the same python as meshroom #install packages in the same python as meshroom
logging.info("Installing packages from "+ cls.envFile) logging.info("Installing packages from "+ nodeDesc.envFile)
buildCommand = sys.executable+" -m pip install "+ cls.envFile buildCommand = sys.executable+" -m pip install "+ nodeDesc.envFile
logging.info("Building with "+buildCommand+" ...") logging.info("Building with "+buildCommand+" ...")
returnValue = os.system(buildCommand) returnValue = os.system(buildCommand)
logging.info("Done") logging.info("Done")
elif cls.envType == EnvType.VENV: elif nodeDesc.envType == EnvType.VENV:
#create venv in default cache folder #create venv in default cache folder
logging.info("Creating virtual env "+os.path.join(defaultCacheFolder, cls._envName)+" from "+cls.envFile) envPath = getVenvPath(nodeDesc._envName)
envPath = os.path.join(defaultCacheFolder, cls._envName) logging.info("Creating virtual env "+envPath+" from "+nodeDesc.envFile)
venv.create(envPath, with_pip=True) venv.create(envPath, with_pip=True)
logging.info("Installing dependencies") logging.info("Installing dependencies")
envExe = getVenvExe(envPath) 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(os.path.dirname(envExe), '..', 'lib')
venvPythonLibFolder = [os.path.join(venvPythonLibFolder, p) venvPythonLibFolder = [os.path.join(venvPythonLibFolder, p)
for p in os.listdir(venvPythonLibFolder) if p.startswith("python")][0] for p in os.listdir(venvPythonLibFolder) if p.startswith("python")][0]
os.symlink(meshroomFolder,os.path.join(venvPythonLibFolder, 'site-packages', 'meshroom')) os.symlink(meshroomFolder,os.path.join(venvPythonLibFolder, 'site-packages', 'meshroom'))
logging.info("Done") logging.info("Done")
elif cls.envType == EnvType.CONDA: elif nodeDesc.envType == EnvType.CONDA:
#build a conda env from a yaml file #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 ; " makeEnvCommand = ( _cleanEnvVarsRez()+" conda config --set channel_priority strict ; "
+" conda env create -v -v --name "+cls._envName +" conda env create -v -v --name "+nodeDesc._envName
+" --file "+cls.envFile+" ") +" --file "+nodeDesc.envFile+" ")
logging.info("Making conda env") logging.info("Making conda env")
logging.info(makeEnvCommand) logging.info(makeEnvCommand)
returnValue = os.system(makeEnvCommand) returnValue = os.system(makeEnvCommand)
#find path to env's folder and add symlink to meshroom #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)\"", +" python -c \"import sys; print(sys.executable)\"",
shell=True).strip().decode('UTF-8') shell=True).strip().decode('UTF-8')
condaPythonLibFolder=os.path.join(os.path.dirname(condaPythonExecudable), '..', 'lib') 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] for p in os.listdir(condaPythonLibFolder) if p.startswith("python")][0]
os.symlink(meshroomFolder,os.path.join(condaPythonLibFolder, 'meshroom')) os.symlink(meshroomFolder,os.path.join(condaPythonLibFolder, 'meshroom'))
logging.info("Done making conda env") logging.info("Done making conda env")
elif cls.envType == EnvType.DOCKER: elif nodeDesc.envType == EnvType.DOCKER:
#build docker image #build docker image
logging.info("Creating image "+cls._envName+" from "+ cls.envFile) logging.info("Creating image "+nodeDesc._envName+" from "+ nodeDesc.envFile)
buildCommand = "docker build -f "+cls.envFile+" -t "+cls._envName+" "+os.path.dirname(cls.envFile) buildCommand = "docker build -f "+nodeDesc.envFile+" -t "+nodeDesc._envName+" "+os.path.dirname(nodeDesc.envFile)
logging.info("Building with "+buildCommand+" ...") logging.info("Building with "+buildCommand+" ...")
returnValue = os.system(buildCommand) returnValue = os.system(buildCommand)
logging.info("Done") logging.info("Done")
else:
raise RuntimeError("Invalid env type")
if returnValue != 0: if returnValue != 0:
raise RuntimeError("Something went wrong during build") 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 Return the command line needed to enter the environment + meshroom_compute
Will make meshroom available in the environment. Will make meshroom available in the environment.
""" """
nodeDesc=chunk.node.nodeDesc
if chunk.node.isParallelized: if chunk.node.isParallelized:
raise RuntimeError("Parallelisation not supported for plugin nodes") raise RuntimeError("Parallelisation not supported for plugin nodes")
if chunk.node.graph.filepath == "": if chunk.node.graph.filepath == "":
raise RuntimeError("The project needs to be saved to use plugin nodes") raise RuntimeError("The project needs to be saved to use plugin nodes")
saved_graph = loadGraph(chunk.node.graph.filepath) saved_graph = loadGraph(chunk.node.graph.filepath)
if (str(chunk.node) not in [str(f) for f in saved_graph._nodes._objects] 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") raise RuntimeError("The changes needs to be saved to use plugin nodes")
cmdPrefix = "" cmdPrefix = ""
@ -322,16 +311,16 @@ class PluginNode(desc.Node):
meshroomComputeArgs = "--node "+chunk.node.name+" \""+chunk.node.graph.filepath+"\"" meshroomComputeArgs = "--node "+chunk.node.name+" \""+chunk.node.graph.filepath+"\""
pythonsetMeshroomPath = "export PYTHONPATH="+meshroomFolder+":$PYTHONPATH;" pythonsetMeshroomPath = "export PYTHONPATH="+meshroomFolder+":$PYTHONPATH;"
if cls.envType == EnvType.VENV: if nodeDesc.envType == EnvType.VENV:
envPath = os.path.join(defaultCacheFolder, cls._envName) envPath = getVenvPath(nodeDesc._envName)
envExe = getVenvExe(envPath) envExe = getVenvExe(envPath)
#make sure meshroom in in pythonpath and that we call the right python #make sure meshroom in in pythonpath and that we call the right python
cmdPrefix = _cleanEnvVarsRez()+pythonsetMeshroomPath+" "+envExe + " "+ meshroomCompute +" " 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 #NOTE: system env vars are not passed to conda run, we installed it 'manually' before
cmdPrefix = _cleanEnvVarsRez()+" conda run --cwd "+os.path.join(meshroomFolder, "..")\ cmdPrefix = _cleanEnvVarsRez()+" conda run --cwd "+os.path.join(meshroomFolder, "..")\
+" --no-capture-output -n "+cls._envName+" "+" python "+meshroomCompute +" --no-capture-output -n "+nodeDesc._envName+" "+" python "+meshroomCompute
elif cls.envType == EnvType.DOCKER: elif nodeDesc.envType == EnvType.DOCKER:
#path to the selected plugin #path to the selected plugin
classFile=inspect.getfile(chunk.node.nodeDesc.__class__) classFile=inspect.getfile(chunk.node.nodeDesc.__class__)
pluginDir = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(classFile),".."))) 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" envCommand = " --env PYTHONPATH=/meshroomFolder --env MESHROOM_NODES_PATH=/meshroomPlugin"
#adds the gpu arg if needed #adds the gpu arg if needed
runtimeArg="" runtimeArg=""
if cls.gpu != desc.Level.NONE: if chunk.node.nodeDesc.gpu != desc.Level.NONE:
runtimeArg="--runtime=nvidia --gpus all" runtimeArg="--runtime=nvidia --gpus all"
#compose cl #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+"\"" meshroomComputeArgs="--node "+chunk.node.name+" "+chunk.node.graph.filepath+"\""
else:
raise RuntimeError("NodeType not recognised")
command=cmdPrefix+" "+meshroomComputeArgs command=cmdPrefix+" "+meshroomComputeArgs
return command return command
#class that call command line nodes in an env # you may use these to explicitly define Pluginnodes
class PluginCommandLineNode(PluginNode, desc.CommandLineNode): class PluginNode(desc.Node):
def buildCommandLine(self, chunk): pass
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 class PluginCommandLineNode(desc.CommandLineNode):
#so in the case of command line using python, we have to make sur it is using the correct python pass
if self.envType == EnvType.VENV:
envPath = os.path.join(defaultCacheFolder, self._envName)
envExe = getVenvExe(envPath)
cmd=cmd.replace("python", envExe)
return cmd

View file

@ -2,7 +2,7 @@
{ {
"pluginName":"Meshroom Research", "pluginName":"Meshroom Research",
"pluginUrl":"https://github.com/alicevision/MeshroomResearch/", "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, "isCollection":true,
"nodeTypes":["Python", "Docker", "Conda"] "nodeTypes":["Python", "Docker", "Conda"]
} }

View file

@ -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 { Item {
id: computeManager 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 { header: RowLayout {
spacing: 0 spacing: 0
MaterialToolButton { MaterialToolButton {
@ -741,6 +825,18 @@ Page {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: removeImagesFromAllGroupsAction.tooltip 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 { } MenuSeparator { }
Action { Action {
@ -1214,6 +1310,17 @@ Page {
var n = _reconstruction.upgradeNode(node) var n = _reconstruction.upgradeNode(node)
_reconstruction.selectedNode = n _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)
}
}
} }
} }
} }

View file

@ -19,6 +19,9 @@ Item {
property bool readOnly: node.locked property bool readOnly: node.locked
/// Whether the node is in compatibility mode /// Whether the node is in compatibility mode
readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false 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 /// Mouse related states
property bool mainSelected: false property bool mainSelected: false
property bool selected: false property bool selected: false
@ -28,7 +31,7 @@ Item {
property point position: Qt.point(x, y) property point position: Qt.point(x, y)
/// Styling /// Styling
property color shadowColor: "#cc000000" 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 color baseColor: defaultColor
property point mousePosition: Qt.point(mouseArea.mouseX, mouseArea.mouseY) 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 // Data sharing indicator
// Note: for an unknown reason, there are some performance issues with the UI refresh. // 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 // Example: a node duplicated 40 times will be slow while creating another identical node

View file

@ -18,9 +18,12 @@ Panel {
property bool readOnly: false property bool readOnly: false
property bool isCompatibilityNode: node && node.compatibilityIssue !== undefined property bool isCompatibilityNode: node && node.compatibilityIssue !== undefined
property string nodeStartDateTime: "" 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 attributeDoubleClicked(var mouse, var attribute)
signal upgradeRequest() signal upgradeRequest()
signal doBuild()
title: "Node" + (node !== null ? " - <b>" + node.label + "</b>" + (node.label !== node.defaultLabel ? " (" + node.defaultLabel + ")" : "") : "") title: "Node" + (node !== null ? " - <b>" + node.label + "</b>" + (node.label !== node.defaultLabel ? " (" + node.defaultLabel + ")" : "") : "")
icon: MaterialLabel { text: MaterialIcons.tune } 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 { Loader {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true

View 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()
}
}
}
}
}

View file

@ -9,6 +9,7 @@ AttributePin 1.0 AttributePin.qml
AttributeEditor 1.0 AttributeEditor.qml AttributeEditor 1.0 AttributeEditor.qml
AttributeItemDelegate 1.0 AttributeItemDelegate.qml AttributeItemDelegate 1.0 AttributeItemDelegate.qml
CompatibilityBadge 1.0 CompatibilityBadge.qml CompatibilityBadge 1.0 CompatibilityBadge.qml
ToBuildBadge 1.0 ToBuildBadge.qml
CompatibilityManager 1.0 CompatibilityManager.qml CompatibilityManager 1.0 CompatibilityManager.qml
singleton GraphEditorSettings 1.0 GraphEditorSettings.qml singleton GraphEditorSettings 1.0 GraphEditorSettings.qml
TaskManager 1.0 TaskManager.qml TaskManager 1.0 TaskManager.qml

View file

@ -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 // Check if document has been saved
function ensureSaved(callback) function ensureSaved(callback)
{ {

View file

@ -586,6 +586,14 @@ class Reconstruction(UIGraph):
localFile = prepareUrlLocalFile(url) localFile = prepareUrlLocalFile(url)
return installPlugin(localFile) 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): def onGraphChanged(self):
""" React to the change of the internal graph. """ """ React to the change of the internal graph. """
self._liveSfmManager.reset() self._liveSfmManager.reset()

View file

@ -6,6 +6,7 @@ from meshroom.core.graph import Graph
logging = logging.getLogger(__name__) logging = logging.getLogger(__name__)
def test_pluginNodes(): def test_pluginNodes():
#Dont run the tests in the CI as we are unable to install plugins beforehand
if "CI" in os.environ: if "CI" in os.environ:
return return
graph = Graph('') graph = Graph('')