How to correctly rotate a QGraphicsItem around different anchors in Qt C++
Asked Answered
P

2

4

I am working on a custom QGraphicsItem that has two anchor points, and I want to be able to rotate the item around these anchors when the user interacts with them. I have implemented a mousePressEvent and mouseMoveEvent to detect which anchor the user clicked on, set the rotation anchor point, and compute the angle of rotation.

Here is a simplified version of my code:

MyView.h

static constexpr float ANCHOR_RADIUS = 10;

class MyView : public QGraphicsItem {
public:
    MyView(float xPos, float yPos, float width, float height, QGraphicsItem *parent = nullptr)
        : _width(width), _height(height), _viewState(VIEW) {
        setPos(xPos, yPos);

        setFlag(ItemIsMovable);

        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(width - diameter, 0, diameter, diameter);
    }

    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override {
        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(_width - diameter, 0, diameter, diameter);

        // Pin 1 and 2 coordinate
        auto c1 = _anchor1.center();
        auto c2 = _anchor2.center();

        painter->drawLine(static_cast<int> (c1.x()), static_cast<int>(c1.y()),
                          static_cast<int>(c2.x()), static_cast<int>(c2.y()));

        painter->drawRect(boundingRect());

        painter->drawEllipse(_anchor1);
        painter->drawEllipse(_anchor2);
    }

    [[nodiscard]] QRectF boundingRect() const override {
        return {0, 0, static_cast<qreal>(_width), static_cast<qreal>(_height)};
    }

    enum ViewState {
        ANCHOR1, ANCHOR2, VIEW
    };

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
        _tapPoint = event->pos();

        auto cp1 = _anchor1.center(); // get center point of anchor 1
        auto cp2 = _anchor2.center(); // get center point of anchor 2

        // Anchor 1 clicked
        if (_anchor1.contains(_tapPoint)) {
            setTransformOriginPoint(cp2.x(), cp2.y()); // set rotation anchor to anchor 2
            _viewState = ANCHOR1;
        }
            // Anchor 2 clicked
        else if (_anchor2.contains(_tapPoint)) {
            setTransformOriginPoint(cp1.x(), cp1.y()); // set rotation anchor to anchor 1
            _viewState = ANCHOR2;
        }
            // View clicked
        else {
            QGraphicsItem::mousePressEvent(event);
            _viewState = VIEW;
        }
    }
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override {
        auto p = event->pos();

        switch (_viewState) {
            case ANCHOR1: {

                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor2.y(), _width));
                setRotation(rotation() - angle); // rotate the item around anchor 2

                break;
            }
            case ANCHOR2: {
                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor1.y(), _width));
                setRotation(rotation() + angle); // rotate the item around anchor 1
                break;
            }
            default:
                QGraphicsItem::mouseMoveEvent(event); // move the item normally
        }
    }

private:
    float _width, _height;
    QRectF _anchor1, _anchor2;
    QPointF _tapPoint;
    ViewState _viewState;
};

main.cpp

static constexpr int WIDTH = 500;
static constexpr int HEIGHT = 500;

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QGraphicsScene scene;
    scene.setSceneRect(QRectF(0, 0, WIDTH, HEIGHT));

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);

    QVBoxLayout layout;
    layout.addWidget(&view);

    QWidget widget;
    widget.setLayout(&layout);

    MyView myView(100, 100, 200, 20);
    scene.addItem(&myView);

    widget.show();

    return QApplication::exec();
}

However, when I try to rotate the item from one anchor point (around the other) and then rotate it again from the other anchor point, it jumps back to the initial position! I am not sure why this is happening.

As you can see in this video, when I first rotate the view it works, but when I try to rotate it from the other anchor, its position jumps to another position!

enter image description here

This is what I am trying to achieve (created with the GeoGebra tool): enter image description here

The solution needs to be applicable to any shape drawn within the MyView::paint() function, rather than being limited to just a line. Although there is an online solution available here, it only works for a line, and similarly, @kenash0625's solution also only works for a line.

Question: What could be causing this issue, and how can I modify my code to achieve the desired behavior of smoothly rotating around different anchor points?

Platelet answered 5/4, 2023 at 21:27 Comment(5)
In your mouseMoveEvent-method you refere in both cases to _anchor2Narco
@Narco thanks for pointing it out, even with this fix the same issue still occursPlatelet
have you ever tried the mousereleaseevent for saving last position of anchor?Pronator
Do you have git repo with this so I can clone and build. For first look I think problem is that setRotation has impact how coordinates are transformed and you are assuming thy are always same.Herne
@MarekR Thank you for your response. I came across a similar question on this page, but the solution presented by Stampede is only applicable to a line item, whereas my scenario involves various item types - the line was simply included for the sake of simplicity. Here is a GitHub repository here.Platelet
C
2

this one should be applicable to any shape drawn within the MyView::paint() function

I made 2 change to your code

  1. add call to QGraphicsItem::setTransformations(const QList<QGraphicsTransform *> &transformations)

  2. change from

auto angle = qRadiansToDegrees(qAtan2(p.y()- _anchor2.y() , _width));

to

auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), _width));

here is edited code:


#include<QGraphicsItem>
#include<QPainter>
#include<QGraphicsSceneMouseEvent>
#include<QGraphicsScene>
#include<QGraphicsView>
#include <QGraphicsRotation>
#include<qmath.h>
static constexpr float ANCHOR_RADIUS = 10;

class MyView : public QGraphicsItem {
public:
    MyView(float xPos, float yPos, float width, float height, QGraphicsItem* parent = nullptr)
        : _width(width), _height(height), _viewState(VIEW) {
        setPos(xPos, yPos);

        setFlag(ItemIsMovable);

        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(width - diameter, 0, diameter, diameter);
    }

    void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override {
        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(_width - diameter, 0, diameter, diameter);

        // Pin 1 and 2 coordinate
        auto c1 = _anchor1.center();
        auto c2 = _anchor2.center();

        painter->drawLine(static_cast<int> (c1.x()), static_cast<int>(c1.y()),
            static_cast<int>(c2.x()), static_cast<int>(c2.y()));

        painter->drawRect(boundingRect());

        painter->drawEllipse(_anchor1);
        painter->drawEllipse(_anchor2);
    }

    [[nodiscard]] QRectF boundingRect() const override {
        return { 0, 0, static_cast<qreal>(_width), static_cast<qreal>(_height) };
    }

    enum ViewState {
        ANCHOR1, ANCHOR2, VIEW
    };

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent* event) override {
        _tapPoint = event->pos();

        // Anchor 1 clicked
        if (_anchor1.contains(_tapPoint)) {
            _viewState = ANCHOR1;
        }
        // Anchor 2 clicked
        else if (_anchor2.contains(_tapPoint)) {
            _viewState = ANCHOR2;
        }
        // View clicked
        else {
            QGraphicsItem::mousePressEvent(event);
            _viewState = VIEW;
        }
    }
    void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override {
        auto p = event->pos();

        auto cp1 = _anchor1.center(); // get center point of anchor 1
        auto cp2 = _anchor2.center(); // get center point of anchor 2

        switch (_viewState) {
        case ANCHOR1: {
            // calculate the angle of the rotation based on the mouse touch
            auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), _width));
            QGraphicsRotation* rot = new QGraphicsRotation;
            rot->setOrigin(QVector3D(cp2.x(), cp2.y(), 0));
            rot->setAxis(Qt::ZAxis);
            rot->setAngle(angle);
            _trans.push_back(rot);
            setTransformations(_trans);
            break;
        }
        case ANCHOR2: {
            // calculate the angle of the rotation based on the mouse touch
            auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor1.y(), _width));
            QGraphicsRotation* rot = new QGraphicsRotation;
            rot->setOrigin(QVector3D(cp1.x(), cp1.y(), 0));
            rot->setAxis(Qt::ZAxis);
            rot->setAngle(angle);
            _trans.push_back(rot);
            setTransformations(_trans);
            break;
        }
        default:
            QGraphicsItem::mouseMoveEvent(event); // move the item normally
        }
    }

private:
    float _width, _height;
    QRectF _anchor1, _anchor2;
    QPointF _tapPoint;
    ViewState _viewState;
    QList<QGraphicsTransform*> _trans;
};

static constexpr int WIDTH = 500;
static constexpr int HEIGHT = 500;

int main7(int argc, char* argv[]) {
    QApplication a(argc, argv);

    QGraphicsScene scene;
    scene.setSceneRect(QRectF(0, 0, WIDTH, HEIGHT));

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);

    QVBoxLayout layout;
    layout.addWidget(&view);

    QWidget widget;
    widget.setLayout(&layout);

    MyView myView(100, 100, 200, 20);
    scene.addItem(&myView);

    widget.show();

    return QApplication::exec();
}
#include"FileName.moc"
Chantilly answered 19/4, 2023 at 2:8 Comment(5)
Thanks @kenash0625, The solution you provide will only work for a line If I draw something else, it won't be rotated. I am drawing a line here only for simplicity but in reality, I will be drawing something else. that's why I use setRotation() which rotates the whole view including its bounds.Platelet
edited. use only setrotation rotate whole viewChantilly
I want to express my gratitude to @kenash0625. After trying the edited version, I noticed that when I release the mouse, the rotation is reset and only the line retains its rotation, while everything else returns to 0, which is not the desired result. Therefore, I have edited the question to provide more clarity.Platelet
this one should be applicable to any shape drawn within the MyView::paint() function. try setTransformations(const QList<QGraphicsTransform *> &transformations). @PlateletChantilly
Excellent! Could you kindly share some details about the solution you applied? I'm facing a similar issue with the Android platform here and understanding how you resolved this problem could potentially assist me in resolving my own. Thank you in advance for taking the time to help me out!Platelet
L
0

Cannot add a comment so I'm posting this as an answer, it may help

Looks like the position of these items aren't correctly set when moving around is done, so you can try setting it manually;

Use mouseReleaseEvent() to know when the positioning is done.

Get the locations of items using scenePos().

And to make sure they stay in the position that is set by the user, use setPos() function.


edit:

Example usage(note that this is only pseudocode):

//We will be here when user releases the mouse
void MyView::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
    
    //Lets call first item _anchor1.
    //Set position of _anchor1 with setPos(),
    //giving the argument of _anchor1's current position using scenePos().
    _anchor1.setPos(_anchor1.scenePos());

    //lets call second item _anchor2
    //Set position of _anchor1 with setPos(),
    //giving the argument of _anchor1's current position using scenePos().
    _anchor2.setPos(_anchor2.scenePos());

}

this may or may not work.

Labialize answered 11/4, 2023 at 11:18 Comment(5)
Maybe the scenePos() function won't work correctly too but worth trying.Labialize
Thanks for answering! but which value should I use in setPos()?Platelet
@Platelet the one that you get from scenePos(). When mouseReleaseEvent() is triggered, it means that user is done with positioning because they released the mouse. At that time you will get the coordinates for both items with scenePos(), use that coordinates in setPos() to make sure they stay in position. I will edit the answer with example usage.Labialize
@Platelet have you been able to try it? curious with this one if it's gonna work or not.Labialize
@kaan_kaya Unfortunately, it did not work for me. The new position must be determined using the previous rotation angle because I have observed that it jumps based on the angle. If no rotation is applied, there is no issue.Platelet

© 2022 - 2024 — McMap. All rights reserved.