Scaling a rotated item based on top left moves the item
Asked Answered
S

2

1

I would like to be able to rotate a QGraphicsItem based on its center, and scale it based on the top left corner.

When I try to combine rotation and scaling, the item also apparently moves...

#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsTextItem>

void testTransformations(QGraphicsScene* s)
{
    qreal angle = 30, scaleX = 2, scaleY = 1;
    // Reference rotated not scaled
    QGraphicsTextItem* ref = new QGraphicsTextItem("bye world");
    ref->setFont(QFont("Arial", 20));
    ref->setDefaultTextColor(Qt::green);
    s->addItem(ref);
    qreal center0X = ref->boundingRect().center().x();
    qreal center0Y = ref->boundingRect().center().y();
    QTransform t0;
    t0.translate(center0X, center0Y);
    t0.rotate(angle);
    t0.translate(-center0X, -center0Y);
    ref->setTransform(t0);

    // Reference scaled not rotated
    QGraphicsTextItem* ref1 = new QGraphicsTextItem("bye world");
    ref1->setFont(QFont("Arial", 20));
    ref1->setDefaultTextColor(Qt::yellow);
    s->addItem(ref1);
    QTransform t;
    t.scale(scaleX, scaleY);
    ref1->setTransform(t);

    // Rotate around center of resized item
    QGraphicsTextItem* yyy = new QGraphicsTextItem("bye world");
    yyy->setDefaultTextColor(Qt::red);
    yyy->setFont(QFont("Arial", 20));
    s->addItem(yyy);
    qreal center1X = yyy->boundingRect().center().x() * scaleX;
    qreal center1Y = yyy->boundingRect().center().y() * scaleY;
    // in my code I store the item size, either before or after the resize, and use it to determine the center - which is virtually the same thing as this for a single operation
    QTransform t1;
    t1.translate(center1X, center1Y);
    t1.rotate(angle);
    t1.translate(-center1X, -center1Y);
    t1.scale(scaleX, scaleY);
    yyy->setTransform(t1);

    // rotated around center of bounding rectangle
    QGraphicsTextItem* xxx = new QGraphicsTextItem("bye world");
    xxx->setDefaultTextColor(Qt::blue);
    xxx->setFont(QFont("Arial", 20));
    s->addItem(xxx);
    qreal center2X = xxx->boundingRect().center().x();
    qreal center2Y = xxx->boundingRect().center().y();
    QTransform t2;
    t2.translate(center2X, center2Y);
    t2.rotate(angle);
    t2.translate(-center2X, -center2Y);
    t2.scale(scaleX, scaleY);
    xxx->setTransform(t2);
}

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QGraphicsScene s;
    QGraphicsView view(&s);
    s.setSceneRect(-20, -20, 500, 500);
    view.show();
    testTransformations(&s);
    return app.exec();
}

Result:

enter image description here

  • green is rotated, not scaled (or scaled a different amount)
  • yellow is scaled, not rotated
  • blue is scaled, and rotated by the center of bounding rectangle (which is not resized)
  • red is scaled and rotated around center

It is evident now to me that the transformations operate correctly - if I resize and rotate an item, I get first the yellow then the red item.

Yet, what I need, is if an item is already rotated (green) then scaled, to behave like the blue - stretch in the same direction, without jumping, while if an item is first scaled, then rotated, to behave like the red... Even more complicated, the original item (green) may have had scaling applied, so my simple solution of using the bounding rectangle wouldn't work.

I have tried to calculate the change... Always with weird results.

Is it possible to scale a rotated item, based on top left, without also moving it, while also rotating it around its center ?

It may require incremental transformations, and would be odd to get different results based on the order they are applied.

Edit: I have been experimenting with position adjustments, since the transformations have failed, but I have not been able to get a formula for a transform function that will give me smooth visual transition of the type:

1) rotate item (pinned to center)
2) scale item (pinned to top left) without jumping
3) rotate item (pinned to center)

where step 2 would also include an offset in position. I just don't know how to do it.
The way I see it, in the fragment for the "red" transform, I would have o add a mapToScene(somePoint) before and after the transform, and perform a correction (moveBy) based on result.

This would not be a great fix, but still... If only I knew how to adjust the position of the item after resize so it doesn't jump, it would still be a fix...

Sweeten answered 2/9, 2015 at 14:51 Comment(0)
S
0

This is my attempt to solve the problem - it works but it is not a great solution:

  • on any transformation, I store m31() and m32().
  • If transformation is scaling, I offset by change between old and new m31() and m32()
Sweeten answered 24/9, 2015 at 18:33 Comment(0)
A
4

OK, sorry for the late edit, it seems like it is impossible to directly achieve that in Qt.

You can however use simple trigonometry to calculate the offset from the center you get when rotating around the 0,0 origin and manually move the item to compensate for the displacement.

Just imagine a circle with center 0,0 and radius a line from the center of the circle to the center of the item's bounding box. As you rotate the item around the 0,0 origin, the item's bounding box center will always sit on the circle, so you can calculate the offset for a given angle of rotation and then rotate and adjust the position of the item to match the center of its previous state.

Here is a little illustration, as you can see, after rotation the item is moved by the offset in order to make it appear as if it rotated around its center and not around the top left. Naturally, since the origin is still 0,0 it will scale as you intended.

enter image description here

EDIT: Even easier, no trig, just using Qt functionality, this method will rotate the item around its center while the transform origin is top left:

static void offsetRotation(qreal angle, QGraphicsItem * i) {
    QPointF c = i->mapToScene(i->boundingRect().center());
    i->setRotation(angle);
    QPointF cNew = i->mapToScene((i->boundingRect()).center());
    QPointF offset = c - cNew;
    i->moveBy(offset.x(), offset.y());
}

However, I noticed something odd - when the origin is set to top left, scaling doesn't really keep the top left corner in the same place as I expected based on my long experience with graphics design software. Instead the item will slowly drift away as its scale increases. The origin point does seem to have some effect on the scaling, but I would certainly not call it adequate by any measure. So depending on what exactly you want to achieve, you may have to use the same offset adjustment trick for scaling as well. As an item scales, it will keep on reporting the same position, but if you map that to the scene, you will realize it actually changes, so you can track that change and compensate in order to produce adequate behavior with a respective offsetScale method.

All this comes at a cost thou, your coordinates will end up being all over the place mess for the sake of keeping the visual output as expected. This may prove to be a complication later on. One solution would be to create your own "public" coordinates for your items, and internally manage all that mess to normalize the end result.

Hopefully someone else may offer a cleaner solution, from my experience with Qt it seems like the people who worked on the graphics classes have had little to no experience with graphics workflow, no doubt they were excellent programmers, but the end result is Qt's graphics classes are often counter-intuitive or even totally incapable of working in the manner people have come to expect from professional graphics software. Perhaps a more pragmatic mind may offer a better remedy for this problem.

Accordingly answered 23/9, 2015 at 8:2 Comment(8)
That doesn't work, it sets the origin for the entire transformation.Sweeten
Thank you, I have come to the same conclusion myself - the only way to achieve the smooth visual result of scaling a rotated item is to also adjust the position - undesirable but seems unavoidable. Not quite sure what to do with the drift that you also noticed, shown in my sample - I tried to map to scene (0,0) but did not get a difference...Sweeten
I have implemented the mentioned offset in other cases, but I tried to do it and it makes the resize happen around the center...Sweeten
Odd, the correction using the bounding rectangle center makes the scaling happen around center, while correcting offset using the bounding rectangle top left makes the rotation happen around top left.Sweeten
In your images prior to the Edit, you show how the rotation offset is determined - but that is calculated automatically by the Qt transform. The problem one is when a rotated item is resized - the offset between my green and red items.Sweeten
@Sweeten - have you considered moving to QML? It does work better and it is a lot easier and faster to use. QtWidgets are a done deal, they are old, outdated, and will never be improved upon.Accordingly
It is not my call... But I am not doing any widgets, just the "work behind the scenes" which would not be handled by QML anyway, as I understand it. How would QML help in this case...Sweeten
Offered the bounty since it was the only response... and it did help a littleSweeten
S
0

This is my attempt to solve the problem - it works but it is not a great solution:

  • on any transformation, I store m31() and m32().
  • If transformation is scaling, I offset by change between old and new m31() and m32()
Sweeten answered 24/9, 2015 at 18:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.