QTransform graphicsitems shifting when origin changed
-
When i enter 151 in my scale spinbox the anchorpoint is changed from bottomleft to bottomright, but suddenly the rect isn't scaled to where the bottomright had been before on the scaled rect but on where the bottom right had been on the unscaled rect. How to fix this unwanted shifting when setting a new point to scale to? (watch the gif below to see what i mean)
Here is a minimal compilable example:
#include <QApplication> #include <QGraphicsScene> #include <QGraphicsRectItem> #include <QGraphicsEllipseItem> #include <QGraphicsView> #include <QSpinBox> #include <QVBoxLayout> #include <QWidget> #include <QTransform> #include <cmath> class CustomGraphicsScene : public QGraphicsScene { Q_OBJECT public: CustomGraphicsScene(QObject *parent = nullptr) : QGraphicsScene(parent), rotationAngle(0), scaleFactor(1.0) { setSceneRect(0, 0, 900, 900); // Set the scene size // Create the background rectangle to visualize the scene boundaries QGraphicsRectItem *bg = new QGraphicsRectItem(sceneRect()); addItem(bg); // Create the main rectangle item to transform rectItem = addRect(0, 0, 100, 100, QPen(Qt::black), QBrush(Qt::blue)); rectItem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsScenePositionChanges | QGraphicsItem::ItemSendsGeometryChanges); // Create a visual indicator for the rotation point (center of rotation) rotationPoint = new QGraphicsEllipseItem(-5, -5, 10, 10); rotationPoint->setBrush(QBrush(QColor(0, 255, 0, 100))); // Semi-transparent green rotationPoint->setPos(anchorPoint); addItem(rotationPoint); // Create a visual indicator for the bottom-right corner handle BottomRightHandle = new QGraphicsEllipseItem(-5, -5, 10, 10); addItem(BottomRightHandle); // Initialize the anchor point (start with the bottom-left corner of the rectangle) anchorPoint = rectItem->boundingRect().bottomLeft(); // Apply the initial transformation (rotation and scaling) applyTransformation(); } public slots: // Slot to set the rotation angle and apply the transformation void setRotationAngle(int angle) { rotationAngle = angle; applyTransformation(); } // Slot to set the scaling factor and adjust the anchor point void setScaleFactor(int scale) { scaleFactor = scale / 100.0; // Convert the scale value from percentage to a decimal (1.0 = 100%) // Dynamically adjust the anchor point based on the scaling factor if (scale > 150) { anchorPoint = rectItem->boundingRect().bottomRight(); // Move anchor to bottom-right if scaling factor > 150% } else { anchorPoint = rectItem->boundingRect().bottomLeft(); // Keep the anchor at the bottom-left otherwise } // Apply the transformations (rotation and scaling) applyTransformation(); } private: // Apply the rotation and scaling transformations to the rectangle item void applyTransformation() { // Prepare the rotation matrix const double a = qDegreesToRadians(static_cast<double>(rotationAngle)); // Convert angle to radians double sina = sin(a); double cosa = cos(a); qDebug() << "Anchor Point: " << anchorPoint; // Create the scaling transformation matrix QTransform scale(scaleFactor, 0, 0, scaleFactor, 0, 0); // Create the rotation transformation matrix QTransform rotate(cosa, sina, -sina, cosa, 0, 0); // Combine the scaling and rotation transformations QTransform transform = scale * rotate; // Translate the scene to the anchor point (move anchor to the origin) QTransform toOrigin; toOrigin.translate(-anchorPoint.x(), -anchorPoint.y()); // Translate back after applying the transformations QTransform fromOrigin; fromOrigin.translate(anchorPoint.x(), anchorPoint.y()); // Combine the transformations: move to origin, apply scale/rotate, then move back QTransform finalTransform = toOrigin * transform * fromOrigin; // Apply the final transformation to the rectangle item rectItem->setTransform(finalTransform); // Update the positions of the rotation point and the bottom-right handle updateHandlePositions(); } // Update the positions of the rotation point and the bottom-right handle after transformation void updateHandlePositions() { // Set the position of the rotation point (center of rotation) in scene coordinates rotationPoint->setPos(rectItem->mapToScene(anchorPoint)); // Set the position of the bottom-right handle in scene coordinates BottomRightHandle->setPos(rectItem->mapToScene(rectItem->boundingRect().bottomRight())); } QGraphicsRectItem *rectItem; // The main rectangle item QGraphicsEllipseItem *rotationPoint; // The visual rotation point (green circle) QGraphicsEllipseItem *BottomRightHandle; // The visual handle for the bottom-right corner QPointF anchorPoint; // The current anchor point for transformations int rotationAngle; // The current rotation angle (in degrees) double scaleFactor; // The current scale factor (relative to the original size) protected: // Override the mouse move event to update the handle positions while the item is being dragged void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override { updateHandlePositions(); QGraphicsScene::mouseMoveEvent(event); // Continue with default handling } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); // Create the custom scene and view CustomGraphicsScene *scene = new CustomGraphicsScene(); QGraphicsView *view = new QGraphicsView(scene); // Create a spin box for controlling the rotation angle (0 to 360 degrees) QSpinBox *rotationSpinBox = new QSpinBox(); rotationSpinBox->setRange(0, 360); // Angle range: 0 to 360 degrees rotationSpinBox->setValue(0); // Initial value: 0 degrees // Create a spin box for controlling the scale factor (10% to 200%) QSpinBox *scaleSpinBox = new QSpinBox(); scaleSpinBox->setRange(10, 200); // Scale range: 10% to 200% scaleSpinBox->setValue(100); // Initial value: 100% (no scaling) // Connect the spin boxes to the corresponding slots in the scene QObject::connect(rotationSpinBox, SIGNAL(valueChanged(int)), scene, SLOT(setRotationAngle(int))); QObject::connect(scaleSpinBox, SIGNAL(valueChanged(int)), scene, SLOT(setScaleFactor(int))); // Set up the layout for the window QWidget window; QVBoxLayout *layout = new QVBoxLayout(&window); layout->addWidget(view); layout->addWidget(rotationSpinBox); layout->addWidget(scaleSpinBox); // Display the main window window.setWindowTitle("Transformation Control"); window.resize(900, 1000); window.show(); return app.exec(); // Start the application event loop } #include "main.moc"
-
@StudentScripter said in QTransform graphicsitems shifting when origin changed:
How to fix this unwanted shifting when setting a new point to scale to?
You are changing te anchor point while keeping the old transformation... this applies the transformation to the new anchor and therefore your rect jumps/suddenly adjusts to the new anchor.
Depends to what your desired result is, either calculate the transformation backwards or reset it and take the current state (i.e. transformation of the rect) as base for the new anchor.
If I tell you "my position is 100km from Berlin", (Berlin = anchor) and in the next step my anchor changes to London and suddenly I'm 100km from London... well... that's a jump :)
When you want me to stay where I am, but change the reference point only, you have to change the offset/distance ( from anchor/city ) and make it like "900km from London" (in fact I'm still around 100km near Berlin)Makes sense?! :)
BTW:
In case you didn't see (because it's actually hard to see)
The rect exactly behaves as expected... the new anchor and rect, when triggering the shift with> 150
, is exactly where the "old" anchor was without any tranformation ( rotation = 0, scale = 100%)
Would be more clear, if you would have used a button to shift the anchors instead of modifying the transformation to do so. -
@Pl45m4 Well that definitely helped. Thanks alot. <3 Im mapping the "visual" anchor now to prevent that shifting, but still minor shifts appear. Do you have an better way to do it?
I changed this: applyTransformation function
void applyTransformation() {
// Prepare the rotation matrix
const double a = qDegreesToRadians(static_cast<double>(rotationAngle)); // Convert angle to radians
double sina = sin(a);
double cosa = cos(a);// Create the scaling transformation matrix QTransform scale(scaleFactor, 0, 0, scaleFactor, 0, 0); // Create the rotation transformation matrix QTransform rotate(cosa, sina, -sina, cosa, 0, 0); // Compute the current visual position of the rectangle's anchor point QPointF currentVisualAnchor = rectItem->mapToScene(anchorPoint); // Combine the scaling and rotation transformations QTransform transform = scale * rotate; // Translate the scene to the anchor point (move anchor to the origin) QTransform toOrigin; toOrigin.translate(-anchorPoint.x(), -anchorPoint.y()); // Translate back after applying the transformations QTransform fromOrigin; fromOrigin.translate(anchorPoint.x(), anchorPoint.y()); // Combine the transformations: move to origin, apply scale/rotate, then move back QTransform finalTransform = toOrigin * transform * fromOrigin; // Apply the final transformation to the rectangle item rectItem->setTransform(finalTransform); // Adjust the rectangle's position to counter the shift caused by the anchor change QPointF newVisualAnchor = rectItem->mapToScene(anchorPoint); QPointF offset = currentVisualAnchor - newVisualAnchor; rectItem->moveBy(offset.x(), offset.y()); // Update the positions of the rotation point and the bottom-right handle updateHandlePositions(); }
-
@StudentScripter said in QTransform graphicsitems shifting when origin changed:
still minor shifts appear. Do you have an better way to do it?
I would do something like this:
@Pl45m4 said in QTransform graphicsitems shifting when origin changed:
Depends to what your desired result is, either calculate the transformation backwards or reset it and take the current state (i.e. transformation of the rect) as base for the new anchor.
So you store the current transformation (rotation, scale, etc...) relative to the old anchor and then re-create this pose step by step, considering the new anchor.
Feels like an Inverse Kinematics at this point :D
(not quite an easy topic, took a Robotics course during my study)But once this works, you can rotate your item seemlessly around every anchor point.
Have you searched the forum for similar topics already?
I just found this topic, where OP describes the same issue.
A possible solution is posted below, which is kinda hacky, but you could try it.Edit:
You are basically looking for a way to solve this equation:
QGraphicsRectItem item; QTransform x; QTransform y; QPointF anchor_I; QPointF anchor_II; // Find x, which will solve: item.setTransform( x.translate(anchor_II)) == item.setTransform( y.translate(anchor_I) ); /* compared in a geometrical way, not C++ object wise :) also boundingRect() might not work, as it does not take any angle into consideration */
-
@StudentScripter said in QTransform graphicsitems shifting when origin changed:
ok but how to get the anchor point at all if not taking it from the boundingrects corners? I mean the coordinates have to come from somewhere
? ? ?
You decided to use the corners as possible anchors... the anchor could be any point inside or outside of the item's boundings.
You could click with your mouse on your item, take that coordinate and use it to rotate your item around that point.Another thought:
What you've done so far is technically not wrong... due to your design... the way your spinbox and transformation is used, you get that jumping behavior.
As in my example taking Berlin and London as reference points, the reference point in your test app changes, while the values, and therefore also the rotation matrix applied to the rectangle stays the same... (-> 100km from Berlin does not equal 100km from London).
There is no way that you get a smooth transition from that when rotating your rect around X by 45° CW, then change the anchor to a different point (doesn't matter if corner, edge or somewhere inside the rect), while your angle is still at 45°.
Of course 45° CW from X is not equal to 45° from Y, as 100km from Berlin is also not the same as 100km from London.So either you want to keep the angle (and live with that jumping behavior) or you calculate the current pose backwards for the new origin and then you can apply that new rotation/transformation matrix to continue from there.
The rotation matrices are applied linearly... one after another... they are not commutative...
This is why
X_I = A * B * C
is different from
X_II = B * A * C
The cleanest and most universal solution would be to calculate the relation (ie. find the translation/transformation) between the current and the "next" anchor.
For this I think you have to take the offset (distance between those points) and their location (below/above, left/right from center/middle) into consideration... A shift of the origin, a change of the orientation of the axes.Maybe playing around with the Transformation Example helps :)
I did it too yesterday :)All depends on what your desired result is...
Do you want to rotate, change anchor, reset the angle/matrix and use current pose as new starting point for any upcoming operation or do you want to have a seemless rotation... but then you have to find the angle, as 90° CW for anchor A might be 270° CW = 90° CCW for B = A mirrored to X axis -
@Pl45m4 Well this i how far i got and it works smoothly now aslong as its only scaling, but rotation is really tricky. But if im allowed to bother you again may have a look on this one file compilable example. Could you hint me how to account for rotation exactly?
Thanks alot for taking the time! I greatly appreciate that. <3
I couldn't post the code here directly cause the forum considers it as "spam" somehow. So may have a look here pastebin:
https://pastebin.com/13Dc3TL6 -
@StudentScripter said in QTransform graphicsitems shifting when origin changed:
Could you hint me how to account for rotation exactly?
Apply the rotation matrix which translates your current anchor to the next one.
-
@Pl45m4 Aren't i applying the matrix already? I mean i map the bounding rect to scene and asfar as i know scene coordinates do account for transformations.
I also tried this below mapping the transform directly with no better results:
(Could you may post a code snippet with exactly how you mean it, im sorry that i don't just get it from your explanation, tho im really trying to)// Connect the combo box's signal to a lambda function QObject::connect(comboBox, &QComboBox::currentIndexChanged, [&comboBox, ScRectItem, &scene](int index) { int selectedValue = comboBox->currentText().toInt(); wasSwitchSuccessful = false; CornerSwitchCase = selectedValue; QRectF MappedRect = ScRectItem->mapRectToScene(ScRectItem->rect()); QTransform trans2; trans2.translate(0, 0); // Ursprungspunkt (obere rechte Ecke) trans2.rotate(rotationValue); trans2.scale(scaleFactor, scaleFactor); trans2.translate(0, 0); // Zurück zur oberen linken Ecke QRectF transMappedRect = trans2.mapRect(MappedRect); switch(selectedValue){ case 1: //TopLeftAnchor ReferenceCorner = MappedRect.topLeft(); break; case 2: //TopRightAnchor ReferenceCorner = MappedRect.topRight(); break; case 3: //BottomRightAnchor ReferenceCorner = MappedRect.bottomRight(); break; case 4: //BottomLeftAnchor ReferenceCorner = MappedRect.bottomLeft(); break; } qDebug() << "Corner Value: " << selectedValue; });
-
The Transformation Example:
(I modified the default number of 3 sequencial transformations to 5)As you see, with every rotation the coordinate system (the orientation) of each point of your object (here: the house) changes...
So also the anchor points have an orientation and a coordinate system... when you change the anchor from, say, bottom-left to top-right, the rotation angle and offset to represent the same pose of the house/your rect also changes.You have to break down everything into single operations as soon as you change your anchor and then apply them one after another.
In the example the anchor is the center of the item.
[0]-[1]: just shifts/translates by +50 along both axis
[1]-[2]: rotates house by 60°CW around center
[2]-[3]: same here
[3]-[4]: same here
[4]-[5]: and hereIf you would have changed the anchor (add offset, rotate/apply rot mat, take offset) as you do, you have to sum up ( i.e. multiply matrices) all these operations in the "chain".
And in between when moving from one anchor to the next, you have to add the translation for the point itself.
Edit:
While it is possible tosave()
andrestore()
a transformedQPainter
(the example above paints the objects directly), the API ofQGraphicsItem
only allows to set one transformation (setTransform
) at a time to scale, shift, rotate the whole thing....
and this transformation is relative to exactly one origin.
So starting from the initial origin (0/0) by design, you shift to your inital origin (i.e. bottom-left corner = the anchor point you want to start with).
To use to chain of multiple and different transformations you need to shift from your current anchor back to the (0/0) point every time (as you do) and pay respect to any possible rotation/translation relative to former anchor points.
But then from your latest anchor's POV.
This is why you can't get around solving the "puzzle".Search for rotations in R2, R3 (2D, 3D) space with a not fixed(!) object coordinate system (since you move your origin)...
the "world" coordinate system in where we operate (here the scene coords) stays the same and we rotate along the same axis. However we move the usually fixed object orientation when changing the "anchor".
Maybe it helps to move this issue to 3D space (it adds one dimension to the vectors and matrices, but I mean for your understanding).@Pl45m4 said in QTransform graphicsitems shifting when origin changed:
For this I think you have to take the offset (distance between those points) and their location (below/above, left/right from center/middle) into consideration... A shift of the origin, a change of the orientation of the axes.
What I meant here is what is explained in the video... using the "right-hand-rule"...
When moving the anchor (origin of the object's coordinate system) "just like that", the orientation might change as well (and not only the shift/translation is happening).