Hi,
I posted few weeks ago a problem that I had with combining rotation and scaling in a 2D space. Thanks to SigTerm (StackOverflow) and a tutor of geometry I almost make it work. However, I have a translation problem. When the software scales an object using a fixed-point of scaling, the result matrix has a translation, but we (my tutor and I) were not able to see how to include this translation in the code to finally make it work. These result in a unwanted translation of the object.
See image describing the problem Here .... I dont have enough points to post images :-(
Here is the code that performs the transformations:
tnkitemcontainer.h
#ifndef TNKITEMCONTAINER_H
#define TNKITEMCONTAINER_H
#include <QGraphicsPolygonItem>
#include <QtGui>
#include <QGraphicsRectItem>
// This is a QGraphicsPolygonItem class that can contains other QGraphicsItems
// With just polygons in the scene it not make much sence to have it, but it is very usefull when
// the container has Images, Vectors or Texts
class tnkitemcontainer : public QObject, public QGraphicsPolygonItem
{
Q_OBJECT
public:
enum { Type = UserType + 38 };
tnkitemcontainer();
int type() const { return Type; }
void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0 );
void setSize(float width, float height);
void embedItem(QGraphicsItem *item);
//Start of procedures that hanble rotation and scale
bool isRotated(){return rotated;}; //Returns true if the last action was rotating
bool isScaled(){return scaled;}; //Return true if the last action was saling
//Sets the starting vector of pivot point and the start position of the cursor.
void setTransStartVectors(QPointF pivotVector,QPointF startVector);
// This procedure scales using the current position of the mouse in scene coordinates
// that then are mapped into items coordinates
// The procedure is called each time the mouse move and holding the left button
void tnkScale(QPointF currentVector);
//This fuction is called when the user releases the left mouse button
//The function applies the translation of the temporary polygon to this item
void finishScale();
// This procedure rotates using the current position of the mouse in scene coordinates
// that then are mapped into items coordinates
// The procedure is called each time the mouse move and holding the left button
void tnkRotate(QPointF currentVector);
//This fuction is called when the user releases the left mouse button
//The function applies the translation of the temporary polygon to this item
void finishRotation();
//End of procedures that handle rotation and scale
private:
QGraphicsPolygonItem *polygonItem; //The embedded polygon
void printMatrix(QTransform matrix); //Function that prints the current matrix
QGraphicsPolygonItem *tmpPolygon; //Red dotted polygon that is shown when scaling or rotating
//Here comes the declaration of iternal procedures
void applyTransform(float rotation,float xFactor, float yFactor);
//Here is the declaration of varibles that handle scale and rotation
QPointF transPivotPoint; //Pivot point of scalig or rotation
QPointF transStartPoint; //Starting point of scaling or rotation
bool rotated; //If rotation is performed
bool scaled; //if scaling is performed
float currentRotation; //The current rotation angle
float formerRotation; //The last rotation angle
float currentXFactor; //The current X scaling factor
float currentYFactor; //The current Y scaling factor
float formerXFactor; //The last X scaling factor
float formerYFactor; //The last Y scaling factor
float formerXTranslation;
float formerYTranslation;
};
#endif // TNKITEMCONTAINER_H
tnkitemcontainer.cpp
#include "tnkitemcontainer.h"
static const float pi = 3.14159265f;
tnkitemcontainer::tnkitemcontainer()
{
/*
The constructor of the container. Here we set the initial state of internal variables
*/
QPen pen;
pen.setStyle(Qt::NoPen); //The container line will not be painted
setPen(pen);
//Sets the starting values for rotation and scale
currentRotation = 0;
formerRotation = 0;
currentXFactor = 1;
formerXFactor = 1;
currentYFactor = 1;
formerYFactor = 1;
rotated = false;
scaled = false;
//Construct the temporary polygon that is shown when scaling or rotating
tmpPolygon = new QGraphicsPolygonItem;
tmpPolygon->setFlag(QGraphicsItem::ItemIsMovable, true);
tmpPolygon->setFlag(QGraphicsItem::ItemIsSelectable, true);
tmpPolygon->setVisible(false);
tmpPolygon->setZValue(1000);
QPen pen2;
pen2.setStyle(Qt::DotLine);
pen2.setColor(QColor("RED"));
tmpPolygon->setPen(pen2);
formerXTranslation = 0;
formerYTranslation = 0;
}
//**********************Start of procedures that handle scaling and rotation***********************
// No idea what this does. I just popied from SigTerm example from StackOverflow
inline QVector2D perp(const QVector2D v){
return QVector2D(-v.y(), v.x());
}
// This function applies a transformation to the temporary polygon each time the mouse moves while
// holding the left mouse button
void tnkitemcontainer::applyTransform(float rotation,float xFactor, float yFactor)
{
tmpPolygon->setTransform(QTransform().translate(transPivotPoint.x(), transPivotPoint.y()).rotate(rotation).scale(xFactor, yFactor).translate(-transPivotPoint.x(), -transPivotPoint.y()));
}
// This function sets the starting values for pivotPoint and starting mouse position
// The fuction is execute when the user clicks on a box for rotation or scaling
// pivotVector: Pivot point in scene coordinates
// startVector: Starting position of the mouse in scene coordinates
void tnkitemcontainer::setTransStartVectors(QPointF pivotVector,QPointF startVector)
{
transPivotPoint = this->mapFromScene(pivotVector); //Translate the pivot point into local coordinates
transStartPoint = this->mapFromScene(startVector); //Translate the starting point into local coordinates
// The following lines of code shows the red dotted polygon in the screen that
// the user see rotating or scaling
// !!!!!!!! Here is where the funny translation happens.... Why?
tmpPolygon->setPolygon(polygonItem->polygon()); //Sets the current polygon to the temporary polygon
this->scene()->addItem(tmpPolygon); //Add the temporary polygon to the scene
tmpPolygon->setVisible(true); //Set the temporary polygon visible
tmpPolygon->setPos(this->scenePos()); //Set the position of the temporary polygon to match this item
tmpPolygon->setTransform(this->transform()); //Set the same matrix of this item to the temporary item
qDebug() << "Matrix set to tmpPolygon";
printMatrix(tmpPolygon->transform());
}
// This function performs an scaling using the current position of the mouse.
// The function is executed each time the mouse move while holding the left mouse button
void tnkitemcontainer::tnkScale(QPointF currentVector)
{
if(!scaled) scaled = true; //Set the scaling variable to true
QPointF currentVectorMapped; //Declaration of the point in item coordinates
currentVectorMapped = this->mapFromScene(currentVector); //Maps the current position in item coordinates
float xDistance; //X distance between the current position of the mouse and the starting position mouse
float yDistance; //Y distance between the current position of the mouse and the starting position mouse
xDistance = currentVectorMapped.x() - transStartPoint.x(); //Calculates the X distance
if (transStartPoint.x() < 0) //If the starting point is on the negative side of the X plane we do an inverse of the value
{
xDistance = xDistance * -1;
}
yDistance = currentVectorMapped.y() - transStartPoint.y(); //Calculates the Y distance
if (transStartPoint.y() < 0) //If the starting point is on the negative side of the Y plane we do an inverse of the value
{
yDistance = yDistance * -1;
}
// Because the distances are affected by the current matrix
// we need to multiply the distances by the current scaling factors
// otherwise they are not in the same proportion for dilatation and contraction
xDistance = xDistance * formerXFactor;
yDistance = yDistance * formerYFactor;
//Calculate the factors by dividing the distances againts the original size of this container
float xFactor;
float yFactor;
xFactor = xDistance / boundingRect().width();
yFactor = yDistance / boundingRect().height();
//Gets the current factors which is previous factor + the calculated factor
currentXFactor = formerXFactor + xFactor;
currentYFactor = formerYFactor + yFactor;
// Applied the transformation to the temporary polygon using the last rotation and the new
// scaling factors
applyTransform(formerRotation,currentXFactor,currentYFactor);
}
// This function is called when the user releses the left button of the mouse
// The function stores the last scaling factors and applied the transformation of the
// the temporary polygon to this item
void tnkitemcontainer::finishScale()
{
scaled = false; //Scaled now is set to false
formerXFactor = currentXFactor; //Gets the last X scaling factor
formerYFactor = currentYFactor; //Gets the last Y scaling factor
// These two lines of code stores the final translation. Translation happens when
// the user dilates the object. The question is: What do I need to do with this translation?
formerXTranslation = tmpPolygon->transform().m31();
formerYTranslation = tmpPolygon->transform().m32();
this->setTransform(tmpPolygon->transform()); //Applies the transformation of the temporary polygon to this item
tmpPolygon->setVisible(true); //Makes the temporary polygon invisible
this->scene()->removeItem(tmpPolygon); //Removes the temporary polgyon from the scene.
qDebug() << "Final Matrix after scaling";
printMatrix(this->transform());
}
void tnkitemcontainer::tnkRotate(QPointF currentVector)
{
if (!rotated) rotated = true; //Set the rotation variable to true
QPointF currentVectorMapped;
currentVectorMapped = this->mapFromScene(currentVector); //Maps the current position of the mouse in item coordinates
// The following code calculates the angle using the starting point, pivot point and the current position
// I copied this code from SigTerm example from StackOverFlow
// In some cases the rotationAngle gets into NAN. This happens when the user moves
// very little positions. I patched the code so NAN is not used
QVector2D startDiff = QVector2D(transStartPoint) - QVector2D(transPivotPoint);
QVector2D endDiff = QVector2D(currentVectorMapped) - QVector2D(transPivotPoint);
startDiff.normalize();
endDiff.normalize();
QVector2D startPerp = perp(startDiff);
float rotationAngle = acos(QVector2D::dotProduct(startDiff, endDiff))*180.0f/pi;
if (QVector2D::dotProduct(startPerp, endDiff) < 0)
rotationAngle = -rotationAngle;
//*********End of code from SigTerm ********************************
//Patch to remove the NAN
QString test(QString::number(rotationAngle));
if (test != "nan")
{
//Gets the current angle which is previous angle + the calculated angle
currentRotation = formerRotation + rotationAngle;
// Applied the transformation to the temporary polygon using the new rotation and the last
// scaling factors
applyTransform(currentRotation,formerXFactor,formerYFactor);
}
}
// This function is called when the user releses the left button of the mouse
// The function stores the last rotation angles and applied the transformation of the
// the temporary polygon to this item
void tnkitemcontainer::finishRotation()
{
rotated = false; //Rotation now is set to falce
formerRotation = currentRotation; //Sets the last rotation angle
// These two lines of code stores the final translation. Translation happens when
// the user rotates the object around a point that is not its center.
// The question is: What do I need to do with this translation?
formerXTranslation = tmpPolygon->transform().m31();
formerYTranslation = tmpPolygon->transform().m32();
this->setTransform(tmpPolygon->transform()); //Applies the transformation of the temporary polygon to this item
tmpPolygon->setVisible(true); //Makes the temporary polygon invisible
this->scene()->removeItem(tmpPolygon); //Removes the temporary polgyon from the scene.
qDebug() << "Final Matrix after rotation";
printMatrix(this->transform());
}
//**********************End of procedures that handle scaling and rotation***********************
void tnkitemcontainer::printMatrix(QTransform matrix)
{
//This is a convenient function that print a matrix to the screen
qDebug() << "|---|---|---|";
qDebug() << "|" << matrix.m11() << "|" << matrix.m12() << "|" << matrix.m13() << "|";
qDebug() << "|" << matrix.m21() << "|" << matrix.m22() << "|" << matrix.m23() << "|";
qDebug() << "|" << matrix.m31() << "|" << matrix.m32() << "|" << matrix.m33() << "|";
qDebug() << "|---|---|---|";
}
void tnkitemcontainer::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
{
//We Modify the paint event so we can remove the dashled line when users selects the item
if (isSelected())
{
QStyleOptionGraphicsItem *newOption = const_cast<QStyleOptionGraphicsItem*>(option);
//Remove the style
newOption->state &= ~QStyle::State_Selected;
QGraphicsPolygonItem::paint(painter, newOption, widget);
}
else
QGraphicsPolygonItem::paint(painter,option,widget);
}
void tnkitemcontainer::setSize(float width, float height)
{
//This will set the size of the polygon based on the size of the polygon
QPolygonF poly;
QPainterPath path;
path.addRect(0-width/2,0-height/2,width,height);
poly = path.toFillPolygon();
setPolygon(poly);
}
void tnkitemcontainer::embedItem(QGraphicsItem *item)
{
//This function will embdeed the polygon into the container
polygonItem = qgraphicsitem_cast<QGraphicsPolygonItem *>(item);
polygonItem->setParentItem(this);
polygonItem->setPos(0,0);
}
Many thanks for your time.