diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 6d8cc2fa85..e862814b41 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -556,9 +556,10 @@ class Node(object): parallelization = None documentation = '' category = 'Other' + coreNode = None - def __init__(self): - pass + def __init__(self, coreNode = None): + self.coreNode = coreNode def upgradeAttributeValues(self, attrValues, fromVersion): return attrValues @@ -599,6 +600,9 @@ class CommandLineNode(Node): parallelization = None commandLineRange = '' + def __init__(self, coreNode = None): + super().__init__(coreNode) + def buildCommandLine(self, chunk): cmdPrefix = '' @@ -665,7 +669,8 @@ class AVCommandLineNode(CommandLineNode): cmdMem = '' cmdCore = '' - def __init__(self): + def __init__(self, coreNode = None): + super().__init__(coreNode) if AVCommandLineNode.cgroupParsed is False: diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 247ba3c6fb..417ddb4967 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()) @@ -487,10 +486,6 @@ def __init__(self, nodeType, position=None, parent=None, **kwargs): self._nodeType = nodeType self.nodeDesc = None - # instantiate node description if nodeType is valid - if nodeType in meshroom.core.nodesDesc: - self.nodeDesc = meshroom.core.nodesDesc[nodeType]() - self.packageName = self.packageVersion = "" self._internalFolder = "" @@ -504,12 +499,17 @@ def __init__(self, nodeType, position=None, parent=None, **kwargs): self._position = position or Position() self._attributes = DictModel(keyAttrName='name', parent=self) self._internalAttributes = DictModel(keyAttrName='name', parent=self) + self._runtimeAttributes = DictModel(keyAttrName='name', parent=self) self.attributesPerUid = defaultdict(set) self._alive = True # for QML side to know if the node can be used or is going to be deleted self._locked = False self._duplicates = ListModel(parent=self) # list of nodes with the same uid self._hasDuplicates = False + # instantiate node description if nodeType is valid + if nodeType in meshroom.core.nodesDesc: + self.nodeDesc = meshroom.core.nodesDesc[nodeType](coreNode=self) + self.globalStatusChanged.connect(self.updateDuplicatesStatusAndLocked) def __getattr__(self, k): @@ -613,6 +613,10 @@ def internalAttribute(self, name): # The internal attribute itself can be returned directly return self._internalAttributes.get(name) + @Slot(str, result=Attribute) + def runtimeAttribute(self, name): + return self._runtimeAttributes.get(name) + def setInternalAttributeValues(self, values): # initialize internal attribute values for k, v in values.items(): @@ -1323,7 +1327,6 @@ def _updateChunks(self): else: self._chunks[0].range = desc.Range() - class CompatibilityIssue(Enum): """ Enum describing compatibility issues when deserializing a Node. diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index b891884e89..d6121c451a 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -506,8 +506,8 @@ class CameraInit(desc.AVCommandLineNode, desc.InitNode): ), ] - def __init__(self): - super(CameraInit, self).__init__() + def __init__(self, coreNode = None): + super(CameraInit, self).__init__(coreNode) def initialize(self, node, inputs, recursiveInputs): # Reset graph inputs diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 306ad1d7e1..4988735ebe 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -2,6 +2,13 @@ from meshroom.core import desc from meshroom.core.utils import VERBOSE_LEVEL +from meshroom.common import Slot +from meshroom.core.attribute import attributeFactory +import os +import threading +import psutil +import time +from contextlib import contextmanager class Meshing(desc.AVCommandLineNode): @@ -537,3 +544,113 @@ class Meshing(desc.AVCommandLineNode): uid=[], ), ] + + def __init__(self, coreNode=None): + super().__init__(coreNode) + + attrDesc = desc.BoolParam( + name="automaticBBoxValid", + label="", + description="", + value=False, + uid=[], + group="" + ) + self.coreNode._runtimeAttributes.add(attributeFactory(attrDesc, None, False, self.coreNode)) + coreNode.globalStatusChanged.connect(self.checkBBox) + + def processChunk(self, chunk): + with boundingBoxMonitor(chunk.node): + super(Meshing, self).processChunk(chunk) + + @Slot() + def checkBBox(self): + """Load automatic bounding box if needed.""" + if self.coreNode.useBoundingBox.value: + return + self.coreNode.runtimeAttribute('automaticBBoxValid').value = False + with boundingBoxMonitor(self.coreNode, checkOnce=True) as thread: + pass + +@contextmanager +def boundingBoxMonitor(node, checkOnce=False): + """ + Context manager to load the automatic bounding box. + + Inputs + ------ + node: MeshingNode + The considered meshing node + + checkOnce: bool + If `True`, the bounding box file will be checked continuously + till created; if already exists, it will be ignored. + Otherwise, the file is checked only once and it will not be + ignored if already created. + + Returns + ------- + BoundingBoxThread + """ + bboxThread = None + try: + if not node.useBoundingBox.value: + bboxThread = BoundingBoxThread(node, checkOnce) + bboxThread.start() + yield bboxThread + finally: + if bboxThread is not None: + bboxThread.stopRequest() + bboxThread.join() + +class BoundingBoxThread(threading.Thread): + """Thread that loads the bounding box.""" + def __init__(self, node, checkOnce): + threading.Thread.__init__(self) + self.node = node + self.checkOnce = checkOnce + self.parentProc = psutil.Process() # by default current process pid + self._stopFlag = threading.Event() + self.interval = 5 # wait duration before rechecking for bounding box file + + def run(self): + self.startTime = time.time() if not self.checkOnce else -1 + try: + while True: + updated = self.updateBoundingBox() + if updated or self.checkOnce: + return + if self._stopFlag.wait(self.interval): + # stopFlag has been set + # try to update boundingBox one last time and exit main loop + if self.parentProc.is_running(): + self.updateBoundingBox() + return + except (KeyboardInterrupt, SystemError, GeneratorExit, psutil.NoSuchProcess): + pass + + def updateBoundingBox(self) -> bool: + """Tries to load the bounding box. + + Returns + ------- + bool: indicates if loading was successful + """ + file = os.path.join(os.path.dirname(self.node.outputMesh.value), "boundingBox.txt") + if not os.path.exists(file) or os.path.getmtime(file) < self.startTime: + return False + with open(file, 'r') as stream: + # file contains (in order, one value per line): + # translation: x, y, z ; rotation: x, y, z ; scale: x, y, z + data = list(map(float, stream.read().strip().splitlines())) + i = 0 + for vec in self.node.boundingBox.value: + for x in vec.value: + x.value = data[i] + i += 1 + self.node.runtimeAttribute('automaticBBoxValid').value = True + return True + + def stopRequest(self): + """ Request the thread to exit as soon as possible. """ + self._stopFlag.set() diff --git a/meshroom/ui/qml/Viewer3D/EntityWithGizmo.qml b/meshroom/ui/qml/Viewer3D/EntityWithGizmo.qml index fe485d19b4..bbb3d2962b 100644 --- a/meshroom/ui/qml/Viewer3D/EntityWithGizmo.qml +++ b/meshroom/ui/qml/Viewer3D/EntityWithGizmo.qml @@ -18,6 +18,7 @@ Entity { property Layer frontLayerComponent property var window property alias uniformScale: transformGizmo.uniformScale // By default, if not set, the value is: false + property alias gizmoEnabled : transformGizmo.enabled property TransformGizmo transformGizmo: TransformGizmo { id: transformGizmo camera: root.camera diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 6674946f35..19086e6205 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -470,7 +470,7 @@ FloatingPane { enabled: model.visible Layout.alignment: Qt.AlignTop Layout.fillHeight: true - text: MaterialIcons.transform + text: MaterialIcons.transform_ font.pointSize: 10 ToolTip.text: model.displayBoundingBox ? "Hide BBox" : "Show BBox" flat: true diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index aa60488d08..6ee942e300 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -167,8 +167,12 @@ Entity { // Specific properties to the MESHING node (declared and initialized for every Entity anyway) property bool hasBoundingBox: { - if (nodeType === "Meshing" && currentNode.attribute("useBoundingBox")) // Can have a BoundingBox + if(nodeType === "Meshing" && currentNode.attribute("useBoundingBox")) // Can have a BoundingBox + { + if(currentNode.runtimeAttribute("automaticBBoxValid") !== undefined) + return currentNode.attribute("useBoundingBox").value || currentNode.runtimeAttribute("automaticBBoxValid").value return currentNode.attribute("useBoundingBox").value + } return false } onHasBoundingBoxChanged: model.hasBoundingBox = hasBoundingBox diff --git a/meshroom/ui/qml/Viewer3D/MeshingBoundingBox.qml b/meshroom/ui/qml/Viewer3D/MeshingBoundingBox.qml index 3f9e0b7a3a..f5cc138f42 100644 --- a/meshroom/ui/qml/Viewer3D/MeshingBoundingBox.qml +++ b/meshroom/ui/qml/Viewer3D/MeshingBoundingBox.qml @@ -18,6 +18,7 @@ Entity { EntityWithGizmo { id: boundingBoxEntity + gizmoEnabled: root.currentMeshingNode ? root.currentMeshingNode.attribute("useBoundingBox").value : true sceneCameraController: root.sceneCameraController frontLayerComponent: root.frontLayerComponent window: root.window