Ole-M Ole-M - 2 months ago 11
C++ Question

check if point exists in QPainterPath

I have an application where I place several curves in my scene. I was looking for an easy way to detect if the user pressed on the line.

boundingRect()
and
intersects()
were too inaccurate when I was having multiple lines drawn. So I made this function, which works like a dream except when the lines are vertical.

selectionMargin
is a global variable set by the user (default = 0.5). It adjusts the margin for how accurate the selection check should be. Names are based on the linear function for each subline,
y = ax + b
. Pos is the position from the mousePressEvent.

bool GraphApp::pointInPath(QPainterPath path, QPointF pos)
{
qreal posY = pos.y();
qreal posX = pos.x();

for (int i = 0; i < path.elementCount()-1; ++i) {
if (posX < path.elementAt(i + 1).x && posX > path.elementAt(i).x) {
qreal dy = path.elementAt(i + 1).y - path.elementAt(i).y;
qreal dx = path.elementAt(i + 1).x - path.elementAt(i).x;
qreal a = dy / dx;
qreal b = path.elementAt(i).y - (path.elementAt(i).x * a);

if (selectionMargin == 0.0)
selectionMargin = 0.5;

qreal lowerBound = (a * posX + b) + selectionMargin;
qreal upperBound = (a * posX + b) - selectionMargin;

if (posY < lowerBound && posY > upperBound)
return true;
}
}
return false;
}


So it seems like this function returns false when I send a mousePressEvent from the area coverd by the vertical lines. My first thought is the if-sentence:

if (posX < path.elementAt(i + 1).x && posX > path.elementAt(i).x)


Any other ideas for how I can implement this without the if-sentence?

I have also seen other people struggling with finding a nice way to check if a
QPainterPath
contains a point without the
boundingRect()
and
intersects()
functions, so this is maybe to use for other people as well :)

EDIT: As far as I know,
contains()
uses
boundingRect()
. So I would not see that as a proper solution

Answer

I once needed something similar than you. I needed to test two paths for similarity. Therefore I created a path from a list of points (I hope you don't need a more complex path since this solution would become extremely more difficult for general QPaintingPaths). This path is constructed using a given "tolerance", this is your selectionMargin.

The function returns a QPainterPath which "draws a region around the given polyline". This region can then be filled and would result in the same image as drawing the original polyline using a pen width of tolerance using round cap and round join options.

You can also, and this is what you want to do, check if a given point is contained in this path. Note that QPainterPath::contains checks for a point to lie within the closed region defined by the path. E.g., this closed region is empty for a single line segment and a triangle for two line segments, so this is not what you want if you use contains directly on your path (as I mentioned in the 3rd comment to your question).

QPainterPath intersectionTestPath(QList<QPointF> input, qreal tolerance)
{
    //will be the result
    QPainterPath path;

    //during the loop, p1 is the "previous" point, initially the first one
    QPointF p1 = input.takeFirst(); 

    //begin with a circle around the start point
    path.addEllipse(p1, tolerance, tolerance); 

    //input now starts with the 2nd point (there was a takeFirst)
    foreach(QPointF p2, input) 
    {
        //note: during the algorithm, the pair of points (p1, p2)
        //      describes the line segments defined by input.

        //offset = the distance vector from p1 to p2
        QPointF offset = p2 - p1;

        //normalize offset to length of tolerance
        qreal length = sqrt(offset.x() * offset.x() + offset.y() * offset.y());
        offset *= tolerance / length;

        //"rotate" the offset vector 90 degrees to the left and right
        QPointF leftOffset(-offset.y(), offset.x());
        QPointF rightOffset(offset.y(), -offset.x());

        //if (p1, p2) goes downwards, then left lies to the left and
        //right to the right of the source path segment
        QPointF left1 = p1 + leftOffset; 
        QPointF left2 = p2 + leftOffset;
        QPointF right1 = p1 + rightOffset;
        QPointF right2 = p2 + rightOffset;

        //rectangular connection from p1 to p2
        {
            QPainterPath p;
            p.moveTo(left1);
            p.lineTo(left2);
            p.lineTo(right2);
            p.lineTo(right1);
            p.lineTo(left1);
            path += p; //add this to the result path
        }

        //circle around p2
        {
            QPainterPath p;
            p.addEllipse(p2, tolerance, tolerance);
            path += p; //add this to the result path
        }

        p1 = p2;
    }

    //This does some simplification; you should use this if you call
    //path.contains() multiple times on a pre-calculated path, but
    //you won't need this if you construct a new path for every call
    //to path.contains().
    return path.simplified();
}
Comments