views:

22

answers:

0

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.