Solved How to deal with several transformations over the same QGraphicsItem?
-
Hi!
I am working on a basic QGraphicsItem's editor. Currently I am supporting only two operations: rotation and scale.
This is the code of my implementation:void rotateItem(float angle) { QTransform transform = item->transform(); transform.rotate(current_angle - angle); item->setTransform(transform); } void scaleItem(float factorX, float factorY) { QTransform transform; transform.scale(factorX, factorY); item->setTransform(transform, true); }
If I only use one of these transformations over the same item several times with different values, the result is accurate. My problem occurs when I start mixing both transformations. For example, if I rotate an item 180 degrees and then I try to scale it (or vice versa), its shape becomes into a kind of distorted figure (sometimes the item becomes a flat line). The result gets even worst as I try to apply more transformations of these types.
As far as I understand, every item transformation is applied over the previous one, so I wonder if there is an accurate way to handle several transformations with the same item without getting distorted results like this:
http://www.maefloresta.com/portal/files/bug.pngAny advice is very welcome, thank you! :)
-
@xtingray
Hi,void scaleItem(float factorX, float factorY) { QTransform transform; transform.scale(factorX, factorY); item->setTransform(transform, true); }
Shouldn't this be:
void scaleItem(float factorX, float factorY) { QTransform transform = item->transform(); //< Aren't you applying a transformation over the existing one? transform.scale(factorX, factorY); item->setTransform(transform, true); }
Also, consider using QGraphicsItem::setRotation and QGraphicsItem::setScale instead.
Kind regards.
-
@kshegunov
thats what the boolean parameter of setTransform should already do.@xtingray
i also struggle mostly with the QTransfrom.What i then try is to store the values and reapply the whole transformation again.
Where i simply chain the operations.item->resetTransform(); item->setTransform( QTransform().translate(centerX, centerY).rotate( angle).translate(-centerX,-centerY) );
-
@raven-worx
Yes, you're right.@xtingray
You can disregard my babbling ... -
Hi! I made some tests and I'm not able to reproduce this. Can you confirm that the following minimal code works for you:
void MainWindow::on_pushButton_clicked() { QGraphicsScene *scene = new QGraphicsScene(ui->graphicsView); ui->graphicsView->setScene(scene); QGraphicsPixmapItem *item = new QGraphicsPixmapItem; QPixmap pm("/home/pw/Downloads/rune.png"); item->setPixmap(pm); scene->addItem(item); // rotate { const qreal angle = 180; QTransform transform; transform.rotate(angle); item->setTransform(transform,true); } // scale { const qreal factorX = 0.5; const qreal factorY = 0.5; QTransform transform; transform.scale(factorX, factorY); item->setTransform(transform, true); } }
-
@Wieland Your code works perfectly but unfortunately for me, my implementation gets the transformation parameters from mouse events, which means, user interaction.
In example, when I said "if I rotate an item 180 degrees" I meant it using the mouse. So, to show you the wrong behavior in a very explicit way I made this video: https://www.youtube.com/watch?v=KJT-SyYv1scIn my first post I described just the part of my code related to this specific issue, but here you can find the whole class from where I handle all the transformation actions, in case you want to take a look at it:
https://github.com/xtingray/tupi/blob/devel/src/plugins/tools/selection/nodemanager.cppAbout the advice of reseting the previous transformations and applying them again, I ran a little test doing something like this:
operations << newTransform; item->resetTransform(); foreach (QTransform op, operations) { if (op.type() == QTransform::TxScale) item->setTransform(op, true); else item->setTransform(op); }
And I must say that the result was much better, although it still isn't good enough. I'll keep looking for new options and if I found an accurate solution for this problem, I will share it right here.
Thanks! -
You may also consider using
void QGraphicsItem::setTransformations ( const QList<QGraphicsTransform *> & transformations )
That allows you to mange the single transformations individually.
-
@xtingray
Well, I didn't want to make a point of it out of fear of writing something stupid yet again. But you do realize transformations are not always commutative, right? A transformation matrix, as any matrix, represents a linear operator in a certain basis (an affine 2D basis in this case), and as a general rule matrices do not commute, i.e.[A, B] = AB - BA != 0
. This has the nasty consequence that order of transformation may be of matter. A prime example is scaling and translating, it makes all the difference in the world what is done first.That aside I also wanted to make a note of how you handle the transformations. If you call the
NodeManager::scale
and/orNodeManager::rotate
methods on each event you get, you may be introducing huge errors in your transformation matrix. Think about it like this:- rotating by 180 deg introduces 1 arbitrary unit of error
- rotating 180 times by 1 degree would then introduce at least 180 arbitrary units of error
So my advice is to make sure you don't get an ill-conditioned (or at worst singular) transformation matrix because of that little detail.
Kind regards.
-
Looking for a hint to solve my problem, I found this example which makes exactly what I need but working with a QPainter display instead of a QGraphicsScene instance:
http://doc.qt.io/qt-5/qtwidgets-painting-affine-example.html
What I wonder is that the methods used in this implementation are very similar to the code I'm using right now, (except that they still use QMatrix instead of QTransform). For example:
void XFormView::setRotation(qreal r) { qreal old_rot = m_rotation; m_rotation = r; QPointF center(pts->points().at(0)); QMatrix m; m.translate(center.x(), center.y()); m.rotate(m_rotation - old_rot); m.translate(-center.x(), -center.y()); pts->setPoints(pts->points() * m); update(); }
I will try to change the interface of my application to confirm if I can get a better result, closer to this example. I'll let you know about the on-going advances :)
-
As part of my investigation, I extracted the basic code of the feature I am fixing and created a "hello world" application that can reproduce the error, in case some else wants to try it.
The source code is available at: https://github.com/xtingray/image.testYou just have to import an image from the File menu. To scale, just press and move any of the corner nodes. To rotate, double click on the center node and press and move any of the corner nodes. To get back to the scale mode, double click in the center node again and that's it.
As far as I can see, the behavior of transformations in the QPainter class is pretty clean, and it doesn't matter how far you mix the scale/rotate operations, the result is always accurate.
On the other hand, dealing with QGraphicsItems and Transformations is another story and yes, I already understood how important is the order you use to apply every operation and how the final result is affected by that. But, as my editor is a user interface allowing full free interaction, I can't define or limit the transformations sequence.For now, I will keep trying another approaches. Suggestions about alternative solutions are very welcome, thank you! :)
-
Finally I could find the solution to my problem in this forum:
http://stackoverflow.com/questions/32186798/resizing-and-rotating-a-qgraphicsitem-results-in-odd-shapeThis is the key code you have to pay attention to in case you need to implement a similar requirement like mine (scaling and rotating QGraphicsItems) :
void scale(float sx, float sy) { QTransform transform; QPointF point = item->boundingRect().center(); transform.translate(point.x(), point.y()); transform.rotate(rotation); transform.scale(sx, sy); transform.translate(-point.x(), -point.y()); item->setTransform(transform); } void rotate(double angle) { QTransform transform; QPointF point = item->boundingRect().center(); transform.translate(point.x(), point.y()); transform.rotate(angle); transform.scale(scaleX, scaleY); transform.translate(-point.x(), -point.y()); item->setTransform(transform); }
To run a very basic but functional example using those methods, please take a look to this code:
https://github.com/xtingray/image.testThank you all for your help! :)
-
@xtingray
Thanks for reporting your findings!