Search code examples
pythonpyqtpyside6

Smooth rotation of QGraphicsObject


    from PySide6.QtCore import QRect, QPoint, QRectF, Qt
    from PySide6.QtGui import QMouseEvent, QBrush, QColor, QPen, QTransform, QGuiApplication
    from PySide6.QtWidgets import QGraphicsObject, QGraphicsItem
    
    
    class ImageBox(QGraphicsObject):
        def __init__(self, pixmap):
            super().__init__()
            self.image = pixmap
            self.mousePressPos = None
            self.mousePressRect = None
            self.setAcceptHoverEvents(True)
            self.setFlag(QGraphicsItem.ItemIsMovable, True)
            self.setFlag(QGraphicsItem.ItemIsSelectable, True)
            self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
    
            self.isMirroredHorizontally = False
            self.isMirroredVertically = False
            self.rotate_op = False
    
            self.rect = self.image.rect()
    
            self.moving = []
            self.origin = QPoint()
    
            self.setTransformOriginPoint(self.rect.center())
    
        def getPixmap(self):
            return self.image
    
        def mirrorHorizontally(self):
            rect = self.boundingRect()
            self.isMirroredHorizontally = not self.isMirroredHorizontally
            scaleX = -1 if self.isMirroredHorizontally else 1
            scaleY = -1 if self.isMirroredVertically else 1
            transform = QTransform(scaleX, 0, 0, scaleY, 0, 0)
            self.setTransform(transform)
            if self.isMirroredHorizontally:
                self.moveBy(rect.width(), 0)
            else:
                self.moveBy(-rect.width(), 0)
    
        def mirrorVertically(self):
            rect = self.boundingRect()
            self.isMirroredVertically = not self.isMirroredVertically
            scaleX = -1 if self.isMirroredHorizontally else 1
            scaleY = -1 if self.isMirroredVertically else 1
            transform = QTransform(scaleX, 0, 0, scaleY, 0, 0)
            self.setTransform(transform)
            if self.isMirroredVertically:
                self.moveBy(0, rect.height())
            else:
                self.moveBy(0, -rect.height())
    
        def corners_rect(self) -> list:
            """ Return corner rect geometry for each corner"""
            size = 10  # Розмір елементів масштабування
            size2 = self.rect.width() - 40  # Ширина контейнера
            size3 = self.rect.height() - 40  # Висота контейнера
            half_size = size / 2  # Половина розміру елементів масштабування
            return [
                QRect(self.rect.left() - half_size, self.rect.top() - half_size, size, size),  # top left
                QRect(self.rect.right() - half_size, self.rect.top() - half_size, size, size),  # top right
                QRect(self.rect.left() - half_size, self.rect.bottom() - half_size, size, size),  # bottom left
                QRect(self.rect.right() - half_size, self.rect.bottom() - half_size, size, size),  # bottom right
                QRect(self.rect.left() + 20, self.rect.top() - half_size, size2, size),
                # top edge
                QRect(self.rect.right() - half_size, self.rect.top() + 20, size, size3),
                # right edge
                QRect(self.rect.left() + 20, self.rect.bottom() - half_size, size2, size),
                # bottom edge
                QRect(self.rect.left() - half_size, self.rect.top() + 20, size, size3),
                # left edge
            ]
    
        def boundingRect(self) -> QRectF:
            """ Override boundingRect """
            return self.rect.adjusted(-10, -10, 10, 10)
    
        def paint(self, painter, option, widget=None):
            painter.drawPixmap(self.rect, self.image)
    
            painter.drawRect(self.rect)
    
            point_list = self.corners_rect()
    
            if self.isSelected():
    
                pen = QPen(QColor("#00FFFF"))  # Set the color of the border
                pen.setWidth(3)  # Set the width of the border
                painter.setPen(pen)
                painter.drawRect(self.rect)
    
                painter.setBrush(QBrush(QColor("#00FFFF")))
                painter.setPen(Qt.NoPen)
                for count, rect in enumerate(point_list[:4], start=0):
                    painter.drawEllipse(rect)
    
            self.update()
    
    
        def mousePressEvent(self, event: QMouseEvent):
            """ override mouse Press Event """
            point_list = self.corners_rect()
            self.moving = [rect.contains(QPoint(event.pos().toPoint())) for rect in point_list]
            if any(self.moving):
                self.origin = self.rect.topLeft()
            else:
                super().mousePressEvent(event)
    
        def mouseReleaseEvent(self, event: QMouseEvent):
            """ Override mouse release event """
            self.moving = [False, False, False, False, False, False, False, False]
            super().mouseReleaseEvent(event)
    
        def rotateWithMouse(self, mouse_position):
            center = self.rect.center()
            angle = math.atan2(mouse_position.y() - center.y(), mouse_position.x() - center.x())
            angle = math.degrees(angle)
            self.setRotation(angle)
    
        def mouseMoveEvent(self, event: QMouseEvent):
            """ Override mouse move event """
            if any(self.moving):
                # If moving is set from mousePressEvent , change geometry
                self.prepareGeometryChange()
    
                pos = event.pos().toPoint()
    
                self.rotate_op = False
    
                if QGuiApplication.keyboardModifiers() == Qt.ShiftModifier:
                    self.rotate_op = True
                    self.rotateWithMouse(event.pos())
    
                if self.rotate_op == False:
                    if self.moving[0] or self.moving[2]:  # top left or bottom left
                        self.rect.setLeft(pos.x())
                    if self.moving[0] or self.moving[1]:  # top left or top right
                        self.rect.setTop(pos.y())
                    if self.moving[1] or self.moving[3]:  # top right or bottom right
                        self.rect.setRight(pos.x())
                    if self.moving[2] or self.moving[3]:  # bottom left or bottom right
                        self.rect.setBottom(pos.y())
    
                    if self.moving[4]:  # top edge
                        self.rect.setTop(pos.y())
                    if self.moving[6]:  # bottom edge
                        self.rect.setBottom(pos.x())
                        self.rect.setBottom(pos.y())
                    if self.moving[5]:  # right edge
                        self.rect.setRight(pos.x())
                    if self.moving[7]:  # left edge
                        self.rect.setLeft(pos.x())
    
                self.rect = self.rect.normalized()
                self.update()
                return
            else:
                super().mouseMoveEvent(event)

I have a main class QGraphicsView into which I add images as ImageBox(QGraphicsObject) objects. The ImageBox has several QRect points that are used to resize the ImageBox. I added rotation to the mouseMoveEvent method when the "Shift" button is pressed, but as a result, when moving the cursor, QGraphicsObject constantly trembles or returns the rotation to the position that was a few milliseconds ago, in a word - unstable. I want to solve this problem.


Solution

  • You're not considering two important aspects:

    1. the event mouse position is always in local coordinates (including transformations), while setRotation() is based on the parent: the angle you're getting does not consider the current rotation, so you should always call setRotation() considering the current rotation() too.
    2. you must consider the angle between the center of the rectangle and the control point, and subtract it from the angle of the new mouse position;
        def rotateWithMouse(self, mouse_position):
            center = self.rect.center()
            point = self.corners_rect()[self.moving.index(True)]
            pointAngle = math.degrees(math.atan2(
                point.y() - center.y(), point.x() - center.x()))
            angle = math.degrees(math.atan2(
                mouse_position.y() - center.y(), mouse_position.x() - center.x()))
            self.setRotation(self.rotation() + (angle - pointAngle))
    

    Also note that you need to be careful about the following:

    • whenever you change the rectangle geometry, you should always update the transformOriginPoint to the new center;
    • boundingRect() should always return a QRectF, but you're returning the QImage.rect() which is a basic (integer based) QRect; change that to QRectF(self.rect.adjusted(-10, -10, 10, 10));
    • you should call prepareGeometryChange() only when changing the bounding rect of the item, not when applying transformations (like the rotation);
    • you shall never call update within paint(), because it will cause indirect recursion (scheduling unnecessary repaints);