I have this program where i'm drawing a rectangular polygon which can be rotated, scaled and moved using a transform.
I've however bumped into a problem which i don't understand and although i would like to figure it out myself, I've come to realize that it's time to seek some help.
The problem:
when the polygon is rotated to certain angles above 45 degrees, dragging the top left anchor makes it go out of control, ie. it moves further away from the anchor until it goes completely haywire...
Without providing a direct answer to the problem, what kind of knowledge am I missing to solve this? I know the basics of matrices and how they work although I have a hard to visualizing it...
To me the calculations itself seem correct and scaling either X or Y independently works as expected but the combination does not.
To reproduce:
- Hold Ctrl to rotate the polygon
- Drag the top left corner
Here's a minimal example:
// project
QT = core gui
greaterThan(QT_MAJOR_VERSION, 4): QT = widgets
CONFIG = c 11
SOURCES = \
main.cpp
HEADERS = mainwindow.h
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS = target
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QMouseEvent>
#include <QTimer>
#include <QSlider>
#include <QtMath>
enum class MoveMode {
MIDDLE,
TOPLEFT,
TOPRIGHT,
BOTTOMLEFT,
BOTTOMRIGHT,
ROTATION,
SYMMETRY,
NONE
};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow() override { };
void paintMe();
void updateMatrix();
void updateSelectionMatrix();
void updateTransformedRect();
void handleDragging();
void paintGrid(QPainter& painter, QTransform transform);
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
private:
QPointF getSelectionAnchorPoint(QVector<QPointF> selectionPolygon, MoveMode moveMode) const;
QRect mapRect(QRect rect, QTransform transform);
QRectF mSelection;
QPointF currentPoint;
QPointF anchorPoint;
QTransform viewTransform;
QTransform viewInverse;
bool isRotating = false;
bool mouseDragging = false;
bool isTranslating = false;
bool somethingDragged = false;
qreal mRotAngle = 0;
qreal mSelectionRot = 0;
qreal mPreviousAngle = 0;
qreal mScaleX;
qreal mScaleY;
QPointF mOffset;
MoveMode mMoveMode = MoveMode::NONE;
};
#endif // MAINWINDOW_H
// main.cpp
#include "mainwindow.h"
#include <QApplication>
#include <QMainWindow>
#include <QDebug>
#include <QtMath>
#include <QPainter>
#include <QHBoxLayout>
#include <QLine>
class SelectionManager : public QObject
{
public:
SelectionManager() {};
~SelectionManager() {};
const QPolygonF& mySelectionPolygon() { return mSelectionPolygon; }
MoveMode getMoveMode() const { return mMoveMode; }
void setMoveMode(MoveMode moveMode) { mMoveMode = moveMode; }
QPointF currentTransformAnchor() const { return mAnchorPoint; }
QTransform selectionTransform() const { return mSelectionTransform; }
QPointF mapToLocalSpace(QPointF point) const { return mSelectionTransform.map(point); };
QPointF mapFromLocalSpace(QPointF point) const { return mSelectionTransform.inverted().map(point); }
QPolygonF mapToLocalSpace(QPolygonF polygon) const { return mSelectionTransform.map(polygon); }
QPolygonF mapFromLocalSpace(QPolygonF polygon) const { return mSelectionTransform.inverted().map(polygon); }
void setMoveModeForAnchorInRange(QPointF point)
{
QPolygonF transformPoly = mapToLocalSpace(mSelectionPolygon);
const double calculatedSelectionTol = mSelectionTolerance;
MoveMode mode;
if (QLineF(point, transformPoly[0]).length() < calculatedSelectionTol)
{
mode = MoveMode::TOPLEFT;
}
else if (QLineF(point, transformPoly[1]).length() < calculatedSelectionTol)
{
mode = MoveMode::TOPRIGHT;
}
else if (QLineF(point, transformPoly[2]).length() < calculatedSelectionTol)
{
mode = MoveMode::BOTTOMRIGHT;
}
else if (QLineF(point, transformPoly[3]).length() < calculatedSelectionTol)
{
mode = MoveMode::BOTTOMLEFT;
}
else if (transformPoly.containsPoint(point, Qt::WindingFill))
{
mode = MoveMode::MIDDLE;
}
else {
mode = MoveMode::NONE;
}
mMoveMode = mode;
}
void adjustSelection(const QPointF& currentPoint, qreal offsetX, qreal offsetY, qreal rotationOffset)
{
QPointF offset(offsetX, offsetY);
MoveMode moveMode = mMoveMode;
switch (moveMode)
{
case MoveMode::MIDDLE: {
translate(currentPoint - offset);
break;
}
case MoveMode::TOPLEFT:
case MoveMode::TOPRIGHT:
case MoveMode::BOTTOMRIGHT:
case MoveMode::BOTTOMLEFT: {
QPolygonF fixedScalePolygon = anchorTransform().map(mSelectionPolygon);
qreal scaleX = 1;
qreal scaleY = 1;
if (moveMode == MoveMode::TOPLEFT) {
QPolygonF worldSelectionPolygon = mapToLocalSpace(mSelectionPolygon);
QLineF lineYCurrentPointFromBottomLeftAnchor(worldSelectionPolygon[3], currentPoint);
QLineF lineXCurrentPointFromTopRightAnchor(worldSelectionPolygon[1], currentPoint);
QLineF lineY(fixedScalePolygon[3], fixedScalePolygon[0]);
QLineF lineX(fixedScalePolygon[1], fixedScalePolygon[0]);
scaleY = lineYCurrentPointFromBottomLeftAnchor.dy() / lineY.dy();
scaleX = lineXCurrentPointFromTopRightAnchor.dx() / lineX.dx();
}
scale(scaleX, scaleY);
break;
}
case MoveMode::ROTATION: {
rotate(rotationOffset);
break;
}
default:
break;
}
calculateSelectionTransformation();
}
void translate(QPointF newPos)
{
mTranslation = newPos;
}
void rotate(qreal angle)
{
mRotatedAngle = angle;
}
void scale(qreal sX, qreal sY)
{
mScaleX = sX;
mScaleY = sY;
}
qreal angleFromPoint(QPointF point, QPointF anchorPoint) const
{
return qRadiansToDegrees(getDifferenceAngle(mSelectionTransform.map(anchorPoint), point));
}
void setSelection(QRectF rect)
{
mSelectionPolygon = rect;
mSomethingSelected = (rect.isValid() ? true : false);
mScaleX = 1;
mScaleY = 1;
calculateSelectionTransformation();
mClickTransform = mSelectionTransform;
}
void setTransformAnchor(QPointF point)
{
QPointF newPos = mapToLocalSpace(point);
QPointF oldPos = mapToLocalSpace(mAnchorPoint);
mTranslation = mTranslation - oldPos newPos;
mAnchorPoint = point;
}
QTransform anchorTransform()
{
QPointF anchorPoint = mAnchorPoint;
QTransform t;
t.translate(-anchorPoint.x(), -anchorPoint.y());
QTransform t2;
t2.translate(mTranslation.x(), mTranslation.y());
QTransform r;
r.rotate(mRotatedAngle);
QTransform s;
s.scale(1, 1);
return (t * s * r * t2);
}
void calculateSelectionTransformation()
{
QPointF anchorPoint = mAnchorPoint;
QTransform t;
t.translate(-anchorPoint.x(), -anchorPoint.y());
QTransform t2;
t2.translate(mTranslation.x(), mTranslation.y());
QTransform r;
r.rotate(mRotatedAngle);
QTransform s;
s.scale(mScaleX, mScaleY);
mSelectionTransform = (t * s * r * t2);
}
qreal getDifferenceAngle(const QPointF a, const QPointF b) const
{
return qAtan2(b.y() - a.y(), b.x() - a.x());
}
private:
bool mSomethingSelected = false;
QPolygonF mSelectionPolygon;
QPointF mOffset;
qreal mScaleX;
qreal mScaleY;
QPointF mTranslation;
qreal mRotatedAngle = 0.0;
MoveMode mMoveMode = MoveMode::NONE;
QTransform mSelectionTransform;
QTransform mClickTransform;
const qreal mSelectionTolerance = 8.0;
QPointF mAnchorPoint;
};
SelectionManager selectMan;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
selectMan.setSelection(QRectF(100,100, 300, 300));
mScaleX = 1;
mScaleY = 1;
updateMatrix();
setMouseTracking(true);
}
void MainWindow::mousePressEvent(QMouseEvent* event)
{
QPoint pos = event->pos();
QPolygonF selectionPoly = selectMan.mySelectionPolygon();
QPointF curPoint = pos;
selectMan.setMoveModeForAnchorInRange(pos);
QPointF anchor = getSelectionAnchorPoint(selectionPoly, selectMan.getMoveMode());
selectMan.setTransformAnchor(anchor);
QPointF anchorPoint = selectMan.currentTransformAnchor();
mRotAngle = selectMan.angleFromPoint(curPoint, anchorPoint) - mPreviousAngle;
currentPoint = pos;
mOffset = currentPoint;
update();
}
void MainWindow::mouseMoveEvent(QMouseEvent* event)
{
QTransform t = viewInverse;
if (event->buttons() & Qt::LeftButton) {
mouseDragging = true;
}
if (mouseDragging) {
if (event->modifiers() == Qt::CTRL) {
isRotating = true;
} else {
isRotating = false;
}
}
currentPoint = event->pos();
if (mouseDragging) {
handleDragging();
}
}
void MainWindow::handleDragging()
{
qreal newAngle = 0;
if (isRotating) {
selectMan.setMoveMode(MoveMode::ROTATION);
QPolygonF mSelectionPolygon = selectMan.mySelectionPolygon();
QPointF anchorPoint = selectMan.currentTransformAnchor();
newAngle = selectMan.angleFromPoint(currentPoint, anchorPoint) - mRotAngle;
}
selectMan.adjustSelection(currentPoint, mOffset.x(), mOffset.y(), newAngle - mPreviousAngle);
mPreviousAngle = newAngle;
mOffset = currentPoint;
update();
}
QPointF MainWindow::getSelectionAnchorPoint(QVector<QPointF> selectionPolygon, MoveMode moveMode) const
{
QPointF anchorPoint;
if (moveMode == MoveMode::BOTTOMRIGHT)
{
anchorPoint = selectionPolygon[0];
}
else if (moveMode == MoveMode::BOTTOMLEFT)
{
anchorPoint = selectionPolygon[1];
}
else if (moveMode == MoveMode::TOPLEFT)
{
anchorPoint = selectionPolygon[2];
}
else if (moveMode == MoveMode::TOPRIGHT)
{
anchorPoint = selectionPolygon[3];
} else {
anchorPoint = QLineF(selectionPolygon[0], selectionPolygon[2]).pointAt(0.5);
}
return anchorPoint;
}
void MainWindow::mouseReleaseEvent(QMouseEvent* event)
{
handleDragging();
currentPoint = event->pos();
mOffset = currentPoint;
isRotating = false;
mouseDragging = false;
somethingDragged = false;
}
void MainWindow::updateMatrix()
{
QTransform transform;
transform.translate(-this->width()/2,-this->height()/2);
viewTransform = transform;
viewInverse = viewTransform.inverted();
}
void MainWindow::paintEvent(QPaintEvent* )
{
QPainter painter(this);
painter.save();
QPen pen(QColor(180, 220, 255));
painter.setPen(pen);
QTransform selectionT = selectMan.selectionTransform();
painter.setPen(Qt::yellow);
QPolygonF mappedPol = selectionT.map(selectMan.mySelectionPolygon());
painter.drawEllipse(QRectF(currentPoint,QSize(10,10)));
painter.setPen(Qt::blue);
painter.drawPolygon((selectMan.mySelectionPolygon()));
painter.setPen(Qt::red);
painter.drawPolygon(mappedPol);
painter.setPen(QColor(255,100,255));
for (int corner = 0; corner < mappedPol.count()-1; corner ) {
QRectF cornerRect(QPointF(mappedPol[corner]-QPointF(10,10)),QSizeF(20,20));
if (corner == 0) {
painter.setBrush(Qt::yellow);
} else if (corner == 1) {
painter.setBrush(Qt::green);
} else if (corner == 2) {
painter.setBrush(Qt::blue);
} else if (corner == 3) {
painter.setBrush(Qt::red);
}
painter.drawRect(cornerRect);
}
QPolygonF localPolygon = selectMan.mySelectionPolygon();
painter.save();
painter.setBrush(Qt::NoBrush);
for (int corner = 0; corner < localPolygon.count()-1; corner ) {
QRectF cornerRect(QPointF(localPolygon[corner]-QPointF(10,10)),QSizeF(20,20));
if (corner == 0) {
painter.setPen(Qt::yellow);
} else if (corner == 1) {
painter.setPen(Qt::green);
} else if (corner == 2) {
painter.setPen(Qt::blue);
} else if (corner == 3) {
painter.setPen(Qt::red);
}
painter.drawRect(cornerRect);
}
painter.restore();
paintGrid(painter, selectionT);
paintGrid(painter, QTransform());
painter.restore();
}
void MainWindow::paintGrid(QPainter& painter, QTransform transform) {
int left = this->rect().left();
int right = this->rect().right();
int top = this->rect().top();
int bottom = this->rect().bottom();
QPen pen(Qt::lightGray);
pen.setCosmetic(true);
painter.setPen(pen);
painter.setOpacity(0.5);
painter.setBrush(Qt::NoBrush);
painter.setRenderHint(QPainter::Antialiasing, false);
painter.setTransform(transform);
for (int x = left; x < right; x = 20) {
painter.drawLine(x, top, x, bottom);
}
for (int y = top; y < bottom; y = 20) {
painter.drawLine(left, y, right, y);
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.resize(500, 500);
w.show();
return a.exec();
}
CodePudding user response:
Solved the problem!
I was missing some fundamental knowledge about calculating the distance between two points. Found a nice recap of how to do that by using the dot product.
Normally calculating the distance between two points will always result in a positive value but i also needed to be able to scale into negative coordinates, thus what i was looking for was the signed distance.
Here's how i solved it:
Given we define the following
a = currentPos
b = bottomRightAnchor
c = topLeftAnchor
First get the direction the vector of the current position and the bottomRightAnchor, we subtract the two vectors
abDir = (a - b)
Next we need to get the normalized direction between the topLeftAnchor and the bottomRightAnchor, which will give us the direction ranging from -1 to 1
cbDir = (c - b)
normCbDir = sqrt((cbDir.x * cbDir.x) (cbDir.y * cbDir.y))
We can now take the dot product of those vectors which will result in the signed distance.
distance = dot(abDir, normCbDir)
Thus we now have the signed distance from the current position to the bottom right corner.
The code is mostly the same, I only had to rework the logic inside the adjustSelection function.
...
QPolygonF projectedPolygon = mapToTransform(mSelectionPolygon);
QVector2D pointVec = QVector2D(currentPoint);
QVector2D movingAnchor = QVector2D(projectedPolygon[0]);
QVector2D staticAnchor = QVector2D(projectedPolygon[1]);
QVector2D directionVecX = staticAnchor - pointVec;
// Calculates the signed distance
qreal distanceX = QVector2D::dotProduct(directionVecX, (staticAnchor - movingAnchor).normalized());
staticAnchor = QVector2D(projectedPolygon[3]);
QVector2D directionVecY = staticAnchor - pointVec;
qreal distanceY = QVector2D::dotProduct(directionVecY, (staticAnchor - movingAnchor).normalized());
qreal originWidth = mSelectionPolygon[1].x() - mSelectionPolygon[0].x();
qreal originHeight = mSelectionPolygon[3].y() - mSelectionPolygon[0].y();
scaleX = distanceX / originWidth;
scaleY = distanceY / originHeight;
...