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)