在一个QPainterPath路径和一条直线之间找到交点

7

我正在尝试确定一个hitscan弹道路径的交点(基本上是一条线,但在我的示例中,我将其表示为 QPainterPath),与场景中的物品相交。我不确定是否有一种方法可以使用QPainterPathQLineF等提供的函数来找到这个点。下面的代码说明了我的目标:

#include <QtWidgets>

bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
    const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
    if (!itemsInPath.isEmpty()) {
        const QPointF projectileStartPos = projectilePath.elementAt(0);
        float shortestDistance = std::numeric_limits<float>::max();
        QGraphicsItem *closest = 0;
        foreach (QGraphicsItem *item, itemsInPath) {
            QPointF distanceAsPoint = item->pos() - projectileStartPos;
            float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
            if (distance < shortestDistance) {
                shortestDistance = distance;
                closest = item;
            }
        }
        QPainterPath targetShape = closest->mapToScene(closest->shape());
        // hitPos = /* the point at which projectilePath hits targetShape */
        hitPos = closest->pos(); // incorrect; always gives top left
        qDebug() << projectilePath.intersects(targetShape); // true
        qDebug() << projectilePath.intersected(targetShape); // QPainterPath: Element count=0
        // To show that they do actually intersect..
        QPen p1(Qt::green);
        p1.setWidth(2);
        QPen p2(Qt::blue);
        p2.setWidth(2);
        scene->addPath(projectilePath, p1);
        scene->addPath(targetShape, p2);
        return true;
    }
    return false;
}

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QGraphicsView view;
    view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
    QGraphicsScene *scene = new QGraphicsScene;
    view.setScene(scene);
    view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
    target->setTransformOriginPoint(QPointF(12.5, 12.5));
    target->setRotation(35);
    target->setPos(100, 100);

    QPainterPath projectilePath;
    projectilePath.moveTo(200, 200);
    projectilePath.lineTo(0, 0);
    projectilePath.lineTo(200, 200);

    QPointF hitPos;
    if (hit(projectilePath, scene, hitPos)) {
        scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
    }

    scene->addPath(projectilePath, QPen(Qt::DashLine));
    scene->addText("start")->setPos(180, 150);
    scene->addText("end")->setPos(20, 0);

    view.show();

    return app.exec();
}
projectilePath.intersects(targetShape) 返回 true,但是 projectilePath.intersected(targetShape) 返回一个空路径。
有没有一种方法可以实现这个?

你使用的是Qt 4还是Qt 5?在问题中添加更具体的标签可能会很有用。 - Daniel Hedberg
QPainterPath和线的交点:通过x找到QPainterPath的y - Mitch
2个回答

7
作为回答 Intersection point of QPainterPath and line (find QPainterPath y by x) 的解决方案,QPainterPath::intersected() 仅适用于具有填充区域的路径。那里提到的矩形路径技巧可以实现如下:
#include <QtWidgets>

/*!
    Returns the closest element (position) in \a sourcePath to \a target,
    using \l{QPoint::manhattanLength()} to determine the distances.
*/
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
    Q_ASSERT(!sourcePath.isEmpty());
    QPointF shortestDistance = sourcePath.elementAt(0) - target;
    qreal shortestLength = shortestDistance.manhattanLength();
    for (int i = 1; i < sourcePath.elementCount(); ++i) {
        const QPointF distance(sourcePath.elementAt(i) - target);
        const qreal length = distance.manhattanLength();
        if (length < shortestLength) {
            shortestDistance = sourcePath.elementAt(i);
            shortestLength = length;
        }
    }
    return shortestDistance;
}

/*!
    Returns \c true if \a projectilePath intersects with any items in \a scene,
    setting \a hitPos to the position of the intersection.
*/
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
    const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
    if (!itemsInPath.isEmpty()) {
        const QPointF projectileStartPos = projectilePath.elementAt(0);
        float shortestDistance = std::numeric_limits<float>::max();
        QGraphicsItem *closest = 0;
        foreach (QGraphicsItem *item, itemsInPath) {
            QPointF distanceAsPoint = item->pos() - projectileStartPos;
            float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
            if (distance < shortestDistance) {
                shortestDistance = distance;
                closest = item;
            }
        }

        QPainterPath targetShape = closest->mapToScene(closest->shape());
        // QLineF has normalVector(), which is useful for extending our path to a rectangle.
        // The path needs to be a rectangle, as QPainterPath::intersected() only accounts
        // for intersections between fill areas, which projectilePath doesn't have.
        QLineF pathAsLine(projectileStartPos, projectilePath.elementAt(1));
        // Extend the first point in the path out by 1 pixel.
        QLineF startEdge = pathAsLine.normalVector();
        startEdge.setLength(1);
        // Swap the points in the line so the normal vector is at the other end of the line.
        pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
        QLineF endEdge = pathAsLine.normalVector();
        // The end point is currently pointing the wrong way; move it to face the same
        // direction as startEdge.
        endEdge.setLength(-1);
        // Now we can create a rectangle from our edges.
        QPainterPath rectPath(startEdge.p1());
        rectPath.lineTo(startEdge.p2());
        rectPath.lineTo(endEdge.p2());
        rectPath.lineTo(endEdge.p1());
        rectPath.lineTo(startEdge.p1());
        // Visualize the rectangle that we created.
        scene->addPath(rectPath, QPen(QBrush(Qt::blue), 2));
        // Visualize the intersection of the rectangle with the item.
        scene->addPath(targetShape.intersected(rectPath), QPen(QBrush(Qt::cyan), 2));
        // The hit position will be the element (point) of the rectangle that is the
        // closest to where the projectile was fired from.
        hitPos = closestPointTo(projectileStartPos, targetShape.intersected(rectPath));

        return true;
    }
    return false;
}

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QGraphicsView view;
    QGraphicsScene *scene = new QGraphicsScene;
    view.setScene(scene);
    view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
    target->setTransformOriginPoint(QPointF(12.5, 12.5));
    target->setRotation(35);
    target->setPos(100, 100);

    QPainterPath projectilePath;
    projectilePath.moveTo(200, 200);
    projectilePath.lineTo(0, 0);
    projectilePath.lineTo(200, 200);

    QPointF hitPos;
    if (hit(projectilePath, scene, hitPos)) {
        scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
    }

    scene->addPath(projectilePath, QPen(Qt::DashLine));
    scene->addText("start")->setPos(180, 150);
    scene->addText("end")->setPos(20, 0);

    view.show();

    return app.exec();
}

这个方法的精度相当不错(± 1 像素,因为 QLineF::length() 是一个整数),但可能有更简洁的方法来实现同样的效果。


我已经创建了一个建议,希望添加类似的功能:https://bugreports.qt-project.org/browse/QTBUG-32313 - Mitch
@AmusedToDeath - 我已经回滚了你的更改 - 这是对几个月前被接受的答案进行了重大的代码更改 - 如果答案有问题,我建议先讨论或创建自己的答案。 - Krease

3

仅供记录(如果其他人也来到这里)。上面的答案很好。closestPoint函数中只有一个小错误,可能会在第一个点已经是最接近的点时发生。它应该返回elementAt(0)而不是elementAt(0) - target。

以下是修复后的函数:

QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
    Q_ASSERT(!sourcePath.isEmpty());

    QPointF shortestDistance;
    qreal shortestLength = std::numeric_limits<int>::max();

    for (int i = 0; i < sourcePath.elementCount(); ++i) {
        const QPointF distance(sourcePath.elementAt(i) - target);
        const qreal length = distance.manhattanLength();
        if (length < shortestLength) {
            shortestDistance = sourcePath.elementAt(i);
            shortestLength = length;
        }
    }

    return shortestDistance;
}

谢谢! :) 你能否在pastebin中粘贴/编辑我的答案的修改版本,以显示错误?然后我可以验证修复并更新答案。 - Mitch
谢谢!希望这样做可以避免我使用折线近似形状。 - D Left Adjoint to U

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接