From e4ccef21876eb94872dd793dd4a8f34feb524e41 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 10:49:30 +0200 Subject: [PATCH 01/14] [commands] AddNode: resolve link expressions on redo --- meshroom/ui/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index dd765527..46ab656e 100644 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -79,6 +79,7 @@ class AddNodeCommand(GraphCommand): def redoImpl(self): self.node = self.graph.addNewNode(self.nodeType) self.setText("Add Node {}".format(self.node.getName())) + self.node._applyExpr() return True def undoImpl(self): From d4509ec20ee482cd3adbb9a8f8bfa78d085cfa08 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 10:53:49 +0200 Subject: [PATCH 02/14] [commands] new AddEdge/RemoveEdge commands --- meshroom/core/graph.py | 36 +++++++++++++++++++++++++----------- meshroom/ui/commands.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index ecbcb340..f4be944e 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -97,12 +97,13 @@ class Attribute(BaseObject): # only dependent of the linked node uid, so it is independent # from the cache folder which may be used in the filepath. return self.node.uid() - if self.isLink(): + if self.isLink: return self.getLinkParam().uid() if isinstance(self._value, basestring): return hash(str(self._value)) return hash(self._value) + @property def isLink(self): """ If the attribute is a link to another attribute. @@ -113,7 +114,7 @@ class Attribute(BaseObject): return self in self.node.graph.edges.keys() def getLinkParam(self): - if not self.isLink(): + if not self.isLink: return None return self.node.graph.edge(self).src @@ -135,7 +136,7 @@ class Attribute(BaseObject): def getExportValue(self): value = self._value # print('getExportValue: ', self.name(), value, self.isLink()) - if self.isLink(): + if self.isLink: value = '{' + self.getLinkParam().fullName() + '}' return value @@ -144,6 +145,8 @@ class Attribute(BaseObject): valueChanged = Signal() value = Property("QVariant", value.fget, value.fset, notify=valueChanged) isOutput = Property(bool, isOutput.fget, constant=True) + isLinkChanged = Signal() + isLink = Property(bool, isLink.fget, notify=isLinkChanged) class Edge(BaseObject): @@ -602,20 +605,31 @@ class Graph(BaseObject): nodesWithOutput = set([edge.src.node for edge in self.edges]) return set(self._nodes) - nodesWithOutput - def addEdge(self, outputAttr, inputAttr): - assert isinstance(outputAttr, Attribute) - assert isinstance(inputAttr, Attribute) - if outputAttr.node.graph != self or inputAttr.node.graph != self: + def addEdge(self, srcAttr, dstAttr): + assert isinstance(srcAttr, Attribute) + assert isinstance(dstAttr, Attribute) + if srcAttr.node.graph != self or dstAttr.node.graph != self: raise RuntimeError('The attributes of the edge should be part of a common graph.') - if inputAttr in self.edges.keys(): - raise RuntimeError('Input attribute "{}" is already connected.'.format(inputAttr.fullName())) - self.edges.add(Edge(inputAttr, outputAttr)) - inputAttr.valueChanged.emit() + if dstAttr in self.edges.keys(): + raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.fullName())) + edge = Edge(dstAttr, srcAttr) + self.edges.add(edge) + dstAttr.valueChanged.emit() + dstAttr.isLinkChanged.emit() + return edge def addEdges(self, *edges): for edge in edges: self.addEdge(*edge) + def removeEdge(self, dstAttr): + if dstAttr not in self.edges.keys(): + raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.fullName())) + edge = self.edges.pop(dstAttr) + dstAttr.valueChanged.emit() + dstAttr.isLinkChanged.emit() + return edge + def getDepth(self, node): return len(self.dfsNodesOnFinish([node])) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 46ab656e..89943249 100644 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -132,3 +132,38 @@ class SetAttributeCommand(GraphCommand): def undoImpl(self): self.graph.node(self.nodeName).attribute(self.attrName).value = self.oldValue + + +class AddEdgeCommand(GraphCommand): + def __init__(self, graph, src, dst, parent=None): + super(AddEdgeCommand, self).__init__(graph, parent) + self.srcNode, self.srcAttr = src.fullName().split(".") + self.dstNode, self.dstAttr = dst.fullName().split(".") + self.setText("Connect '{}'->'{}'".format(src.fullName(), dst.fullName())) + + def redoImpl(self): + try: + self.graph.addEdge(self.graph.node(self.srcNode).attribute(self.srcAttr), + self.graph.node(self.dstNode).attribute(self.dstAttr)) + except RuntimeError: + return False + return True + + def undoImpl(self): + self.graph.removeEdge(self.graph.node(self.dstNode).attribute(self.dstAttr)) + + +class RemoveEdgeCommand(GraphCommand): + def __init__(self, graph, edge, parent=None): + super(RemoveEdgeCommand, self).__init__(graph, parent) + self.srcNode, self.srcAttr = edge.src.fullName().split(".") + self.dstNode, self.dstAttr = edge.dst.fullName().split(".") + self.setText("Disconnect '{}'->'{}'".format(edge.src.fullName(), edge.dst.fullName())) + + def redoImpl(self): + self.graph.removeEdge(self.graph.node(self.dstNode).attribute(self.dstAttr)) + return True + + def undoImpl(self): + self.graph.addEdge(self.graph.node(self.srcNode).attribute(self.srcAttr), + self.graph.node(self.dstNode).attribute(self.dstAttr)) From 7ddf86e6bc9f5a92aee8e9b7cbb5704b2728be79 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 12:12:00 +0200 Subject: [PATCH 03/14] [ui] homogenize PySide imports --- meshroom/ui/reconstruction.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index b072d64e..3e8a93c4 100644 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -1,32 +1,32 @@ -from PySide2 import QtCore +from PySide2.QtCore import QObject, Slot, Property, Signal from meshroom.core import graph from meshroom.ui import commands -class Reconstruction(QtCore.QObject): +class Reconstruction(QObject): def __init__(self, parent=None): super(Reconstruction, self).__init__(parent) self._graph = graph.Graph("") self._undoStack = commands.UndoStack(self) - @QtCore.Slot(str) + @Slot(str) def addNode(self, nodeType): self._undoStack.tryAndPush(commands.AddNodeCommand(self._graph, nodeType)) - @QtCore.Slot(graph.Node) + @Slot(graph.Node) def removeNode(self, node): self._undoStack.tryAndPush(commands.RemoveNodeCommand(self._graph, node)) - @QtCore.Slot(graph.Attribute, "QVariant") + @Slot(graph.Attribute, "QVariant") def setAttribute(self, attribute, value): self._undoStack.tryAndPush(commands.SetAttributeCommand(self._graph, attribute, value)) - @QtCore.Slot(str) + @Slot(str) def load(self, filepath): self._graph.load(filepath) - undoStack = QtCore.Property(QtCore.QObject, lambda self: self._undoStack, constant=True) - graph = QtCore.Property(QtCore.QObject, lambda self: self._graph, constant=True) - nodes = QtCore.Property(QtCore.QObject, lambda self: self._graph.nodes, constant=True) + undoStack = Property(QObject, lambda self: self._undoStack, constant=True) + graph = Property(graph.Graph, lambda self: self._graph, constant=True) + nodes = Property(QObject, lambda self: self._graph.nodes, constant=True) From d34d6f27ec5def57c0f1b54bf225971d39f1feb6 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 12:21:22 +0200 Subject: [PATCH 04/14] [ui] expose add/removeEdge as Slots --- meshroom/ui/reconstruction.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 3e8a93c4..b52e69b1 100644 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -19,6 +19,14 @@ class Reconstruction(QObject): def removeNode(self, node): self._undoStack.tryAndPush(commands.RemoveNodeCommand(self._graph, node)) + @Slot(graph.Attribute, graph.Attribute) + def addEdge(self, src, dst): + self._undoStack.tryAndPush(commands.AddEdgeCommand(self._graph, src, dst)) + + @Slot(graph.Edge) + def removeEdge(self, edge): + self._undoStack.tryAndPush(commands.RemoveEdgeCommand(self._graph, edge)) + @Slot(graph.Attribute, "QVariant") def setAttribute(self, attribute, value): self._undoStack.tryAndPush(commands.SetAttributeCommand(self._graph, attribute, value)) From 0a69e9f3446d0722c59e1801d54a6abf3529cfdc Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 12:22:29 +0200 Subject: [PATCH 05/14] [ui] update core graph and clear undoStack after loading a mg file --- meshroom/ui/reconstruction.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index b52e69b1..e1291a43 100644 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -34,6 +34,8 @@ class Reconstruction(QObject): @Slot(str) def load(self, filepath): self._graph.load(filepath) + self._graph.update() + self._undoStack.clear() undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graph = Property(graph.Graph, lambda self: self._graph, constant=True) From e494a63c9d07f6cb79ce648d01c4e4273a775622 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 15:17:34 +0200 Subject: [PATCH 06/14] [core] stop graph execution mechanism --- meshroom/core/graph.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index f4be944e..4d0435a7 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -219,6 +219,7 @@ class Node(BaseObject): self.attribute(k)._value = v self.status = StatusData(self.name, self.nodeType()) self.statistics = stats.Statistics() + self._subprocess = None def __getattr__(self, k): try: @@ -407,6 +408,10 @@ class Node(BaseObject): def beginSequence(self): self.upgradeStatusTo(Status.SUBMITTED_LOCAL) + def stopProcess(self): + if self._subprocess: + self._subprocess.terminate() + def process(self): self.upgradeStatusTo(Status.RUNNING) statThread = stats.StatisticsThread(self) @@ -416,20 +421,20 @@ class Node(BaseObject): cmd = self.commandLine() print('\n =====> commandLine:\n', cmd, '\n') print(' - logFile: ', self.logFile()) - self.proc = psutil.Popen(cmd, stdout=logF, stderr=logF, shell=True) + self._subprocess = psutil.Popen(cmd, stdout=logF, stderr=logF, shell=True) # store process static info into the status file self.status.commandLine = cmd # self.status.env = self.proc.environ() # self.status.createTime = self.proc.create_time() - statThread.proc = self.proc - stdout, stderr = self.proc.communicate() - self.proc.wait() + statThread.proc = self._subprocess + stdout, stderr = self._subprocess.communicate() + self._subprocess.wait() - self.status.returnCode = self.proc.returncode + self.status.returnCode = self._subprocess.returncode - if self.proc.returncode != 0: + if self._subprocess.returncode != 0: logContent = '' with open(self.logFile(), 'r') as logF: logContent = ''.join(logF.readlines()) @@ -438,9 +443,11 @@ class Node(BaseObject): except: self.upgradeStatusTo(Status.ERROR) raise - statThread.running = False - # Don't need to join, the thread will finish a bit later. - # statThread.join() + finally: + statThread.running = False + # Don't need to join, the thread will finish a bit later. + # statThread.join() + self._subprocess = None self.upgradeStatusTo(Status.SUCCESS) @@ -548,6 +555,7 @@ class Graph(BaseObject): node._name = self._createUniqueNodeName(node.nodeType()) node.graph = self self._nodes.add(node) + self.stopExecutionRequested.connect(node.stopProcess) # Trigger internal update when an attribute is modified for attr in node.attributes: # type: Attribute @@ -732,6 +740,10 @@ class Graph(BaseObject): self.updateInternals() self.updateStatusFromCache() + def stopExecution(self): + """ Request graph execution to be stopped """ + self.stopExecutionRequested.emit() + @property def nodes(self): return self._nodes @@ -743,6 +755,7 @@ class Graph(BaseObject): nodes = Property(BaseObject, nodes.fget, constant=True) edges = Property(BaseObject, edges.fget, constant=True) + stopExecutionRequested = Signal() def loadGraph(filepath): """ From 81fe36636550f04f9110c469fee4dc8fa920bc43 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 15:20:56 +0200 Subject: [PATCH 07/14] [ui] add execute / stopExecution slots + 'computing' property * graph computation is done in a separate Thread * 'computing' relies on this thread's 'is_alive' status --- meshroom/ui/reconstruction.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index e1291a43..bf4bd4e2 100644 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -1,3 +1,5 @@ +from threading import Thread + from PySide2.QtCore import QObject, Slot, Property, Signal from meshroom.core import graph @@ -10,6 +12,7 @@ class Reconstruction(QObject): super(Reconstruction, self).__init__(parent) self._graph = graph.Graph("") self._undoStack = commands.UndoStack(self) + self._computeThread = Thread() @Slot(str) def addNode(self, nodeType): @@ -37,6 +40,29 @@ class Reconstruction(QObject): self._graph.update() self._undoStack.clear() + @Slot(graph.Node) + def execute(self, node=None): + if self.computing: + return + nodes = [node] if node else self._graph.getLeaves() + self._computeThread = Thread(target=self._execute, args=(nodes,)) + self._computeThread.start() + + def _execute(self, nodes): + self.computingChanged.emit() + graph.execute(self._graph, nodes) + self.computingChanged.emit() + + @Slot() + def stopExecution(self): + if not self.computing: + return + self._graph.stopExecution() + self._computeThread.join() + self.computingChanged.emit() + undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graph = Property(graph.Graph, lambda self: self._graph, constant=True) nodes = Property(QObject, lambda self: self._graph.nodes, constant=True) + computingChanged = Signal() + computing = Property(bool, lambda self: self._computeThread.is_alive(), notify=computingChanged) \ No newline at end of file From 653cec28c9daa167eef0cf3bd70ac5eeed87f949 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 16:31:20 +0200 Subject: [PATCH 08/14] [core] add Node.statusName property --- meshroom/core/graph.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 4d0435a7..ca10b504 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -351,11 +351,12 @@ class Node(BaseObject): """ statusFile = self.statusFile() if not os.path.exists(statusFile): - self.status.status = Status.NONE + self.upgradeStatusTo(Status.NONE) return with open(statusFile, 'r') as jsonFile: statusData = json.load(jsonFile) self.status.fromDict(statusData) + self.statusChanged.emit() def saveStatusFile(self): """ @@ -397,6 +398,7 @@ class Node(BaseObject): print('WARNING: downgrade status on node "{}" from {} to {}'.format(self._name, self.status.status.name, newStatus)) self.status.status = newStatus + self.statusChanged.emit() self.saveStatusFile() def isAlreadySubmitted(self): @@ -457,12 +459,19 @@ class Node(BaseObject): def getStatus(self): return self.status + @property + def statusName(self): + return self.status.status.name + name = Property(str, getName, constant=True) attributes = Property(BaseObject, getAttributes, constant=True) internalFolderChanged = Signal() internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) depthChanged = Signal() depth = Property(int, depth.fget, notify=depthChanged) + statusChanged = Signal() + statusName = Property(str, statusName.fget, notify=statusChanged) + WHITE = 0 GRAY = 1 From 45f04e14d2ecb85a22f5829d0e0c6770c7392323 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 16:31:36 +0200 Subject: [PATCH 09/14] [core] Edge: invert src/dst parameters order --- meshroom/core/graph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index ca10b504..8ad39a0f 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -151,10 +151,10 @@ class Attribute(BaseObject): class Edge(BaseObject): - def __init__(self, dst, src, parent=None): + def __init__(self, src, dst, parent=None): super(Edge, self).__init__(parent) - self._dst = dst self._src = src + self._dst = dst @property def src(self): @@ -629,7 +629,7 @@ class Graph(BaseObject): raise RuntimeError('The attributes of the edge should be part of a common graph.') if dstAttr in self.edges.keys(): raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.fullName())) - edge = Edge(dstAttr, srcAttr) + edge = Edge(srcAttr, dstAttr) self.edges.add(edge) dstAttr.valueChanged.emit() dstAttr.isLinkChanged.emit() From b27db8fc7a5bc64745ea2dafb489ad2fa2b6a907 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 16:35:03 +0200 Subject: [PATCH 10/14] [commands] unify naming on edges source / destination --- meshroom/ui/commands.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 89943249..d5964acf 100644 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -104,13 +104,14 @@ class RemoveNodeCommand(GraphCommand): parent=self.graph, **self.nodeDesc["attributes"] ), self.nodeName) assert (node.getName() == self.nodeName) - # recreate edges deleted on node removal + # edges having this node as destination could be retrieved from node description + # but we're missing edges starting from this node for key, value in self.edges.items(): - iNode, iAttr = key.split(".") - oNode, oAttr = value.split(".") - self.graph.addEdge(self.graph.node(oNode).attribute(oAttr), - self.graph.node(iNode).attribute(iAttr)) + dstNode, dstAttr = key.split(".") + srcNode, srcAttr = value.split(".") + self.graph.addEdge(self.graph.node(srcNode).attribute(srcAttr), + self.graph.node(dstNode).attribute(dstAttr)) node.updateInternals() From bc5bb96c2e06c725729cfaba820ccd5265bdafc9 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 16:38:07 +0200 Subject: [PATCH 11/14] [tests] fix test_depth depth is now a property --- tests/test_task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_task.py b/tests/test_task.py index 148e8e85..464056dd 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -13,9 +13,9 @@ def test_depth(): (tB.output, tC.input) ) - assert tA.getDepth() == 1 - assert tB.getDepth() == 2 - assert tC.getDepth() == 3 + assert tA.depth == 1 + assert tB.depth == 2 + assert tC.depth == 3 if __name__ == '__main__': From 23541db54d2f49eeecb621d0d445afd926c5bcf6 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 17:13:31 +0200 Subject: [PATCH 12/14] [core] stop StatisticsThread when node is processed --- meshroom/core/graph.py | 6 +++--- meshroom/core/stats.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 8ad39a0f..4967de39 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -446,10 +446,10 @@ class Node(BaseObject): self.upgradeStatusTo(Status.ERROR) raise finally: - statThread.running = False - # Don't need to join, the thread will finish a bit later. - # statThread.join() self._subprocess = None + # ask and wait for the stats thread to terminate + statThread.stopRequest() + statThread.join() self.upgradeStatusTo(Status.SUCCESS) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index 7e18a8c7..8de0a4bd 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -191,8 +191,8 @@ class StatisticsThread(threading.Thread): threading.Thread.__init__(self) self.node = node self.proc = None - self.running = True self.statistics = self.node.statistics + self._stopFlag = threading.Event() def updateStats(self): self.lastTime = time.time() @@ -200,7 +200,14 @@ class StatisticsThread(threading.Thread): self.node.saveStatistics() def run(self): - while self.running: + while True: self.updateStats() - time.sleep(60) + if self._stopFlag.wait(60): + # stopFlag has been set + # update stats one last time and exit main loop + self.updateStats() + return + def stopRequest(self): + """ Request the thread to exit as soon as possible. """ + self._stopFlag.set() From af53dd4ae70b4c5d95a1a53af614044e4d6df262 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 18:01:49 +0200 Subject: [PATCH 13/14] [ui] new EdgeMouseArea QtQuick Component QtQuick components providing mouse interaction for edges (cubic splines) --- meshroom/ui/__main__.py | 3 + meshroom/ui/components/__init__.py | 6 + meshroom/ui/components/edge.py | 210 +++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100755 meshroom/ui/components/__init__.py create mode 100755 meshroom/ui/components/edge.py diff --git a/meshroom/ui/__main__.py b/meshroom/ui/__main__.py index b61466d0..6bcf8613 100644 --- a/meshroom/ui/__main__.py +++ b/meshroom/ui/__main__.py @@ -7,6 +7,7 @@ from PySide2.QtQml import QQmlApplicationEngine from meshroom.ui.reconstruction import Reconstruction from meshroom.ui.utils import QmlInstantEngine +from meshroom.ui import components if __name__ == "__main__": @@ -17,6 +18,8 @@ if __name__ == "__main__": engine = QmlInstantEngine() engine.addFilesFromDirectory(qmlDir) engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False)) + components.registerTypes() + r = Reconstruction() engine.rootContext().setContextProperty("_reconstruction", r) diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py new file mode 100755 index 00000000..300492f3 --- /dev/null +++ b/meshroom/ui/components/__init__.py @@ -0,0 +1,6 @@ + +def registerTypes(): + from PySide2.QtQml import qmlRegisterType + from meshroom.ui.components.edge import EdgeMouseArea + + qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") diff --git a/meshroom/ui/components/edge.py b/meshroom/ui/components/edge.py new file mode 100755 index 00000000..9b4dfc60 --- /dev/null +++ b/meshroom/ui/components/edge.py @@ -0,0 +1,210 @@ +from PySide2.QtCore import Signal, Property, QPointF, Qt, QObject +from PySide2.QtGui import QPainterPath, QVector2D +from PySide2.QtQuick import QQuickItem + + +class MouseEvent(QObject): + """ + Simple MouseEvent object, since QQuickMouseEvent is not accessible in the public API + """ + def __init__(self, evt): + super(MouseEvent, self).__init__() + self._x = evt.x() + self._y = evt.y() + self._button = evt.button() + + x = Property(float, lambda self: self._x, constant=True) + y = Property(float, lambda self: self._y, constant=True) + button = Property(Qt.MouseButton, lambda self: self._button, constant=True) + + +class EdgeMouseArea(QQuickItem): + """ + Provides a MouseArea shaped as a cubic spline for mouse interaction with edges. + + Note: for performance reason, shape is updated only when geometry changes since this is the main use-case with edges. + TODOs: + - update when start/end points change too + - review this when using new QML Shape module + """ + def __init__(self, parent=None): + super(EdgeMouseArea, self).__init__(parent) + + self._viewScale = 1.0 + self._startX = 0.0 + self._startY = 0.0 + self._endX = 0.0 + self._endY = 0.0 + self._curveScale = 0.7 + self._edgeThickness = 1.0 + self._hullThickness = 2.0 + self._containsMouse = False + self._path = None # type: QPainterPath + + self.setAcceptHoverEvents(True) + self.setAcceptedMouseButtons(Qt.AllButtons) + + def contains(self, point): + return self._path.contains(point) + + def hoverEnterEvent(self, evt): + self.setContainsMouse(True) + super(EdgeMouseArea, self).hoverEnterEvent(evt) + + def hoverLeaveEvent(self, evt): + self.setContainsMouse(False) + super(EdgeMouseArea, self).hoverLeaveEvent(evt) + + def geometryChanged(self, newGeometry, oldGeometry): + super(EdgeMouseArea, self).geometryChanged(newGeometry, oldGeometry) + self.updateShape() + + def mousePressEvent(self, evt): + if not self.acceptedMouseButtons() & evt.button(): + evt.setAccepted(False) + return + e = MouseEvent(evt) + self.pressed.emit(e) + e.deleteLater() + + def mouseReleaseEvent(self, evt): + e = MouseEvent(evt) + self.released.emit(e) + e.deleteLater() + + def updateShape(self): + p1 = QPointF(self._startX, self._startY) + p2 = QPointF(self._endX, self._endY) + ctrlPt = QPointF(self.ctrlPtDist, 0) + path = QPainterPath(p1) + path.cubicTo(p1 + ctrlPt, p2 - ctrlPt, p2) + + # Compute offset on x and y axis + hullOffset = self._edgeThickness * self._viewScale + self._hullThickness + v = QVector2D(p2 - p1).normalized() + offset = QPointF(hullOffset * -v.y(), hullOffset * v.x()) + + self._path = QPainterPath(path.toReversed()) + self._path.translate(-offset) + path.translate(offset) + self._path.connectPath(path) + + @property + def thickness(self): + return self._hullThickness + + @thickness.setter + def thickness(self, value): + if self._hullThickness == value: + return + self._hullThickness = value + self.thicknessChanged.emit() + + @property + def edgeThickness(self): + return self._edgeThickness + + @edgeThickness.setter + def edgeThickness(self, value): + if self._edgeThickness == value: + return + self._edgeThickness = value + self.thicknessChanged.emit() + + @property + def viewScale(self): + return self._viewScale + + @viewScale.setter + def viewScale(self, value): + if self.viewScale == value: + return + self._viewScale = value + self.viewScaleChanged.emit() + + @property + def startX(self): + return self._startX + + @startX.setter + def startX(self, value): + self._startX = value + self.startXChanged.emit() + + @property + def startY(self): + return self._startY + + @startY.setter + def startY(self, value): + self._startY = value + self.startYChanged.emit() + + @property + def endX(self): + return self._endX + + @endX.setter + def endX(self, value): + self._endX = value + self.endXChanged.emit() + + @property + def endY(self): + return self._endY + + @endY.setter + def endY(self, value): + self._endY = value + self.endYChanged.emit() + + @property + def curveScale(self): + return self._curveScale + + @curveScale.setter + def curveScale(self, value): + self._curveScale = value + self.curveScaleChanged.emit() + self.updateShape() + + @property + def ctrlPtDist(self): + return self.width() * self.curveScale * (-1 if self._startX > self._endX else 1) + + @property + def containsMouse(self): + return self._containsMouse + + def setContainsMouse(self, value): + if self._containsMouse == value: + return + self._containsMouse = value + self.containsMouseChanged.emit() + + thicknessChanged = Signal() + thickness = Property(float, thickness.fget, thickness.fset, notify=thicknessChanged) + edgeThicknessChanged = Signal() + edgeThickness = Property(float, edgeThickness.fget, edgeThickness.fset, notify=edgeThicknessChanged) + viewScaleChanged = Signal() + viewScale = Property(float, viewScale.fget, viewScale.fset, notify=viewScaleChanged) + startXChanged = Signal() + startX = Property(float, startX.fget, startX.fset, notify=startXChanged) + startYChanged = Signal() + startY = Property(float, startY.fget, startY.fset, notify=startYChanged) + endXChanged = Signal() + endX = Property(float, endX.fget, endX.fset, notify=endXChanged) + endYChanged = Signal() + endY = Property(float, endY.fget, endY.fset, notify=endYChanged) + curveScaleChanged = Signal() + curveScale = Property(float, curveScale.fget, curveScale.fset, notify=curveScaleChanged) + ctrlPtDistChanged = Signal() + ctrlPtDist = Property(float, ctrlPtDist.fget, notify=ctrlPtDist) + containsMouseChanged = Signal() + containsMouse = Property(float, containsMouse.fget, notify=containsMouseChanged) + acceptedButtons = Property(int, + lambda self: super(EdgeMouseArea, self).acceptedMouseButtons, + lambda self, value: super(EdgeMouseArea, self).setAcceptedMouseButtons(value)) + + pressed = Signal(MouseEvent) + released = Signal(MouseEvent) From c1b80ff3e3c4065e42ef77c2c4b8c6c7933e2252 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Oct 2017 18:02:39 +0200 Subject: [PATCH 14/14] [ui] stop potential computations when app is about to exit --- meshroom/ui/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meshroom/ui/__main__.py b/meshroom/ui/__main__.py index 6bcf8613..0124b584 100644 --- a/meshroom/ui/__main__.py +++ b/meshroom/ui/__main__.py @@ -23,6 +23,9 @@ if __name__ == "__main__": r = Reconstruction() engine.rootContext().setContextProperty("_reconstruction", r) + # Request any potential computation to stop on exit + app.aboutToQuit.connect(r.stopExecution) + engine.load(os.path.normpath(url)) app.exec_()