Revision control

++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ #include <QTest>
++ #include <QtQuick/QQuickItem>
++
++ #include <QtQuick3D/private/qquick3dviewport_p.h>
++ #include <QtQuick3D/private/qquick3dmodel_p.h>
++ #include <QtQuick3D/private/qquick3dpickresult_p.h>
++ #include <QtQuick3D/private/qquick3ditem2d_p.h>
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderray_p.h>
++
++ #include "../shared/util.h"
++
++ Q_DECLARE_METATYPE(QSSGCullFaceMode)
++
++ class CustomGeometry : public QQuick3DGeometry
++ {
++ Q_OBJECT
++ Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)
++ QML_NAMED_ELEMENT(CustomGeometry)
++
++ public:
++ CustomGeometry(uint count);
++
++ int count() { return m_count; }
++ void setCount(int count);
++
++ signals:
++ void countChanged();
++
++ private:
++ struct Vertex {
++ QVector3D position;
++ QVector3D normal;
++ QVector2D uv;
++ };
++
++ void updateData();
++
++ uint m_count = 500;
++ };
++
++
++ CustomGeometry::CustomGeometry(uint count)
++ {
++ m_count = count;
++ updateData();
++ }
++
++ void CustomGeometry::setCount(int count)
++ {
++ m_count = count;
++ updateData();
++ emit countChanged();
++ }
++
++ void CustomGeometry::updateData()
++ {
++ QVector<Vertex> vertices;
++ QVector<QPoint> coords;
++
++ int rowCount = m_count;
++ int columnCount = m_count;
++ float uvX = 1.0f / float(rowCount - 1);
++ float uvY = 1.0f / float(columnCount - 1);
++
++ for (int i = 0; i < columnCount; i++) {
++ for (int j = 0; j < rowCount; j++) {
++ Vertex v;
++ float x = 4.0f / (columnCount-1) * i - 2.0f;
++ float z = 4.0f / (rowCount-1) * j - 2.0f;
++ v.position = QVector3D(x, .0f, z);
++ v.normal = QVector3D(.0f, .0f, .0f);
++ v.uv = QVector2D(j * uvX, i * uvY);
++ vertices.push_back(v);
++ coords.push_back(QPoint(i, j));
++ }
++ }
++ QVector3D boundsMin = QVector3D(-2.0f, -1.0f, -2.0f);
++ QVector3D boundsMax = QVector3D(2.0f, 1.0f, 2.0f);
++ setBounds(boundsMin, boundsMax);
++
++ QVector<quint32> indices;
++ for (int i = 0; i < columnCount - 1; i++) {
++ for (int j = 0; j < rowCount -1; j++) {
++ indices.push_back(i * rowCount + j);
++ indices.push_back(i * rowCount + j + 1);
++ indices.push_back(i * rowCount + j + rowCount);
++
++ indices.push_back(i * rowCount + j + 1);
++ indices.push_back(i * rowCount + j + 1 + rowCount);
++ indices.push_back(i * rowCount + j + rowCount);
++ }
++ }
++
++ setStride(sizeof(Vertex));
++ setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
++ addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
++ 0,
++ QQuick3DGeometry::Attribute::F32Type);
++ addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
++ sizeof(QVector3D) * 2,
++ QQuick3DGeometry::Attribute::F32Type);
++ addAttribute(QQuick3DGeometry::Attribute::NormalSemantic,
++ sizeof(QVector3D),
++ QQuick3DGeometry::Attribute::F32Type);
++ addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
++ 0,
++ QQuick3DGeometry::Attribute::U32Type);
++
++ QByteArray vertexBuffer(reinterpret_cast<char *>(vertices.data()), vertices.size() * sizeof(Vertex));
++ setVertexData(vertexBuffer);
++
++ QByteArray indexBuffer(reinterpret_cast<char *>(indices.data()), indices.size() * sizeof(quint32));
++ setIndexData(indexBuffer);
++
++ update();
++ }
++
++
++ class tst_Picking : public QQuick3DDataTest
++ {
++ Q_OBJECT
++ private Q_SLOTS:
++ void initTestCase() override;
++ void test_view_picking();
++ void test_ray_picking();
++ void test_object_picking2();
++ void test_item_picking();
++ void test_picking_QTBUG_111997();
++ void test_picking_corner_case();
++ void test_triangleIntersect();
++ void test_sphere_geometry();
++ void test_closest_point_picking();
++ void test_triangleIntersect_cullMode_data();
++ void test_triangleIntersect_cullMode();
++ void test_picking_cull_mode();
++
++ private:
++ QQuickItem *find2DChildIn3DNode(QQuickView *view, const QString &objectName, const QString &itemName);
++
++ };
++
++ void tst_Picking::initTestCase()
++ {
++ QQuick3DDataTest::initTestCase();
++ if (!initialized())
++ return;
++ }
++
++ QQuickItem *tst_Picking::find2DChildIn3DNode(QQuickView *view, const QString &objectName, const QString &itemName)
++ {
++ QQuick3DNode *obj = view->rootObject()->findChild<QQuick3DNode*>(objectName);
++ if (!obj)
++ return nullptr;
++ QQuickItem *subsceneRoot = obj->findChild<QQuickItem *>();
++ if (!subsceneRoot)
++ subsceneRoot = obj->findChild<QQuick3DItem2D *>()->contentItem();
++ if (!subsceneRoot)
++ return nullptr;
++ QObject *child = subsceneRoot->findChild<QObject *>(itemName);
++ return static_cast<QQuickItem *>(child);
++ }
++
++ void tst_Picking::test_view_picking()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("picking.qml"), QSize(400, 400)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++ QQuick3DModel *model2 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model2"));
++ QVERIFY(model2);
++ QQuick3DModel *instancedModel = view3d->findChild<QQuick3DModel *>(QStringLiteral("instancedModel"));
++ QVERIFY(instancedModel);
++ QQuickItem *item2d = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dNode", "item2d"));
++ QVERIFY(item2d);
++
++ const qreal dpr = view->devicePixelRatio();
++ if (dpr != 1.0) {
++ QSKIP("Test uses window positions to get exact values and those assume DPR of 1.0");
++ }
++
++ // Pick nearest based on viewport position
++
++ // Center of model1
++ auto result = view3d->pick(200, 200);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.distance(), 550.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.5f, 0.5f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(result.position(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Upper right corner of model1
++ result = view3d->pick(250, 150);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.distance(), 550.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(1.0f, 1.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(50.0f, 50.0f, 50.0f));
++ QCOMPARE(result.position(), QVector3D(50.0f, 50.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Just outside model1's upper right corner, so should hit the model behind (model2)
++ // This one might cause some floating point grief
++ result = view3d->pick(251, 151);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model2);
++ QCOMPARE(result.distance(), 600.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.5099999904632568, 0.4899999797344208));
++ QCOMPARE(result.scenePosition(), QVector3D(51.0f, 49.0f, -0.000003814697265625));
++ QCOMPARE(result.position(), QVector3D(1.0f, -1.0f, 49.999996185302734f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Upper right corner of model2
++ result = view3d->pick(300, 100);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model2);
++ QCOMPARE(result.distance(), 600.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(1.0f, 1.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(100.0f, 100.0f, 0.0f));
++ QCOMPARE(result.position(), QVector3D(50.0f, 50.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Just outside model2's upper right corner, so there should be no hit
++ result = view3d->pick(301, 99);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QCOMPARE(result.objectHit(), nullptr);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Center of the third entry in the instance table
++ result = view3d->pick(350, 200);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), instancedModel);
++ QCOMPARE(result.distance(), 550.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.0f, 0.5f));
++ QCOMPARE(result.scenePosition(), QVector3D(150.0f, 0.0f, 50.0f));
++ QCOMPARE(result.position(), QVector3D(-50.0f, 0.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 2);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // The bottom right of instancedModel,
++ // overlapped by model1 and then model2, should hit model1
++ result = view3d->pick(225, 175);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.distance(), 550.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.75f, 0.75f));
++ QCOMPARE(result.scenePosition(), QVector3D(25.0f, 25.0f, 50.0f));
++ QCOMPARE(result.position(), QVector3D(25.0f, 25.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Pick one specific based on viewport position
++
++ // Center of model1, overlapping model2,
++ // only check for model2, so should hit model2
++ result = view3d->pick(200, 200, model2);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model2);
++ QCOMPARE(result.distance(), 600.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.0f, 0.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, 0.0f));
++ QCOMPARE(result.position(), QVector3D(-50.0f, -50.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // The bottom right of instancedModel,
++ // overlapped model1 and then model2,
++ // only check for instancedModel, should hit instancedModel
++ result = view3d->pick(225, 175, instancedModel);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), instancedModel);
++ QCOMPARE(result.distance(), 650.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(1.0f, 0.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(25.0f, 25.0f, -50.0f));
++ QCOMPARE(result.position(), QVector3D(50.0f, -50.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 1);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Pick subset based on viewport position
++
++ // Upper right corner of model1, center of model2
++ // Check for model2 only
++ // Should hit model2, ignore model1
++ QJSEngine engine;
++ QJSValue array = engine.newArray(1);
++ array.setProperty(0, engine.newQObject(model2));
++ auto resultList = view3d->pickSubset(250, 150, array);
++ QCOMPARE(resultList.size(), 1);
++ QCOMPARE(resultList[0].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[0].objectHit(), model2);
++ QCOMPARE(resultList[0].distance(), 600.0f);
++ QCOMPARE(resultList[0].uvPosition(), QVector2D(0.5f, 0.5f));
++ QCOMPARE(resultList[0].scenePosition(), QVector3D(50.0f, 50.0f, 0.0f));
++ QCOMPARE(resultList[0].position(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(resultList[0].sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(resultList[0].normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(resultList[0].instanceIndex(), 0);
++ QCOMPARE(resultList[0].itemHit(), nullptr);
++
++ // The bottom right of instancedModel,
++ // overlapped model1 and then model2,
++ // Check for both model2 and instancedModel
++ // Should hit both and ignore model1
++ array = engine.newArray(2);
++ array.setProperty(0, engine.newQObject(model2));
++ array.setProperty(1, engine.newQObject(instancedModel));
++ resultList = view3d->pickSubset(225, 175, array);
++ QCOMPARE(resultList.size(), 2);
++ QCOMPARE(resultList[0].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[0].objectHit(), model2);
++ QCOMPARE(resultList[0].distance(), 600.0f);
++ QCOMPARE(resultList[0].uvPosition(), QVector2D(0.25f, 0.25f));
++ QCOMPARE(resultList[0].scenePosition(), QVector3D(25.0f, 25.0f, 0.0f));
++ QCOMPARE(resultList[0].position(), QVector3D(-25.0f, -25.0f, 50.0f));
++ QCOMPARE(resultList[0].sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(resultList[0].normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(resultList[0].instanceIndex(), 0);
++ QCOMPARE(resultList[0].itemHit(), nullptr);
++ QCOMPARE(resultList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[1].objectHit(), instancedModel);
++ QCOMPARE(resultList[1].distance(), 650.0f);
++ QCOMPARE(resultList[1].uvPosition(), QVector2D(1.0f, 0.0f));
++ QCOMPARE(resultList[1].scenePosition(), QVector3D(25.0f, 25.0f, -50.0f));
++ QCOMPARE(resultList[1].position(), QVector3D(50.0f, -50.0f, 50.0f));
++ QCOMPARE(resultList[1].sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(resultList[1].normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(resultList[1].instanceIndex(), 1);
++ QCOMPARE(resultList[1].itemHit(), nullptr);
++
++
++ // Enable the 2D item
++ item2d->setEnabled(true);
++ QVERIFY(waitForFrames(view.data(), 1));
++ // Then picking on top of model1 should not pick it anymore
++ result = view3d->pick(200, 200);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2d);
++ QCOMPARE(result.distance(), 400.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(75.0f, 75.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, 200.0f));
++ QCOMPARE(result.position(), QVector3D(0.0f, 0.0f, 0.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), -1);
++ QCOMPARE(result.objectHit(), nullptr);
++ // Hide the 2D item
++ item2d->setVisible(false);
++ QVERIFY(waitForFrames(view.data(), 1));
++ // Then picking on top of model1 should pick it again
++ result = view3d->pick(200, 200);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++
++ // Pick all based on viewport position
++
++ // Center of model1, bottom left corner of model2
++ resultList = view3d->pickAll(200, 200);
++ QCOMPARE(resultList.size(), 2);
++ QCOMPARE(resultList[0].objectHit(), model1);
++ QCOMPARE(resultList[1].objectHit(), model2);
++
++ // Add the 2D item
++ item2d->setVisible(true);
++ QVERIFY(waitForFrames(view.data(), 1));
++ resultList = view3d->pickAll(200, 200);
++ QCOMPARE(resultList.size(), 3);
++ QCOMPARE(resultList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultList[0].itemHit(), item2d);
++ QCOMPARE(resultList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[1].objectHit(), model1);
++ QCOMPARE(resultList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[2].objectHit(), model2);
++ // Hide the 2D item
++ item2d->setVisible(false);
++ QVERIFY(waitForFrames(view.data(), 1));
++
++ // Top right corner of model1, center of model2
++ resultList = view3d->pickAll(250, 150);
++ QCOMPARE(resultList.size(), 2);
++ QCOMPARE(resultList[0].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[0].objectHit(), model1);
++ QCOMPARE(resultList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[1].objectHit(), model2);
++
++ // Just outside model1's upper right corner, so should hit the model behind (model2)
++ resultList = view3d->pickAll(251, 151);
++ QCOMPARE(resultList.size(), 1);
++ QCOMPARE(resultList[0].objectHit(), model2);
++
++ // The bottom right of the second entry in the instance table,
++ // overlapping model1 and model2
++ resultList = view3d->pickAll(225, 175);
++ QCOMPARE(resultList.size(), 3);
++ QCOMPARE(resultList[0].objectHit(), model1);
++ QCOMPARE(resultList[1].objectHit(), model2);
++ QCOMPARE(resultList[2].objectHit(), instancedModel);
++ QCOMPARE(resultList[2].instanceIndex(), 1);
++
++ // Just outside model2's upper right corner, so there should be no hit
++ resultList = view3d->pickAll(301, 99);
++ QVERIFY(resultList.isEmpty());
++ }
++
++ void tst_Picking::test_ray_picking()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("picking.qml"), QSize(400, 400)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++ QQuick3DModel *model2 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model2"));
++ QVERIFY(model2);
++ QQuick3DModel *instancedModel = view3d->findChild<QQuick3DModel *>(QStringLiteral("instancedModel"));
++ QVERIFY(instancedModel);
++ QQuickItem *item2d = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dNode", "item2d"));
++ QVERIFY(item2d);
++
++ item2d->setEnabled(true);
++ item2d->setVisible(true);
++ QVERIFY(waitForFrames(view.data(), 1));
++
++ // Ray based picking one result
++
++ // Down the z axis from 0,0,100 (towards 0,0,0)
++ QVector3D origin(0.0f, 0.0f, 100.0f);
++ QVector3D direction(0.0f, 0.0f, -1.0f);
++ auto result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.distance(), 50.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.5f, 0.5f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(result.position(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Down the z axis from 0,0,250 (towards 0,0,0) through the item2d
++ origin = QVector3D(0.0f, 0.0f, 250.0f);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2d);
++ QCOMPARE(result.distance(), 50.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(75.0f, 75.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, 200.0f));
++ QCOMPARE(result.position(), QVector3D(0.0f, 0.0f, 0.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), -1);
++ QCOMPARE(result.objectHit(), nullptr);
++
++ // Up the z axis from 0, 0, -101 (towards 0,0,0)
++ origin = QVector3D(0.0f, 0.0f, -101.0f);
++ direction = QVector3D(0.0f, 0.0f, 1.0f);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model2);
++ QCOMPARE(result.distance(), 1.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(1.0f, 0.0f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, -100.0f));
++ QCOMPARE(result.position(), QVector3D(-50.0f, -50.0f, -50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Up the z axis from 0, 0, -100 (towards 0,0,-1000)
++ origin = QVector3D(0.0f, 0.0f, -100.0f);
++ direction = QVector3D(0.0f, 0.0f, -1.0f);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QCOMPARE(result.objectHit(), nullptr);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Ray based picking all results
++
++ // Down the z axis from 0,0,250 (towards 0,0,0)
++ origin = QVector3D(0.0f, 0.0f, 250.0f);
++ direction = QVector3D(0.0f, 0.0f, -1.0f);
++ auto resultList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultList.size(), 3);
++ QCOMPARE(resultList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultList[0].itemHit(), item2d);
++ QCOMPARE(resultList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[1].objectHit(), model1);
++ QCOMPARE(resultList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[2].objectHit(), model2);
++ QVector3D hitPoint = resultList[2].scenePosition();
++
++ // Test ray picking only model 2
++ result = view3d->rayPick(origin, direction, model2);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model2);
++ QCOMPARE(result.scenePosition(), hitPoint);
++
++ // Up the z axis from 0, 0, -101 (towards 0,0,0)
++ origin = QVector3D(0.0f, 0.0f, -101.0f);
++ direction = QVector3D(0.0f, 0.0f, 1.0f);
++ resultList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultList.size(), 2);
++ QCOMPARE(resultList[0].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[0].objectHit(), model2);
++ QCOMPARE(resultList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultList[1].objectHit(), model1);
++
++ // Up the z axis from 0, 0, -100 (towards 0,0,-1000)
++ origin = QVector3D(0.0f, 0.0f, -100.0f);
++ direction = QVector3D(0.0f, 0.0f, -1.0f);
++ resultList = view3d->rayPickAll(origin, direction);
++ QVERIFY(resultList.isEmpty());
++ }
++
++
++ void tst_Picking::test_object_picking2()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("picking2.qml"), QSize(100, 100)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ const qreal dpr = view->devicePixelRatio();
++ if (dpr != 1.0) {
++ QSKIP("Test uses window positions to get exact values and those assume DPR of 1.0");
++ }
++
++ const auto viewSize = view->size();
++ const float halfWidth = viewSize.width() * 0.5f;
++ const float halfHeight = viewSize.height() * 0.5f;
++ const float horizontalPickLine = halfHeight - 35;
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *coneModel = view3d->findChild<QQuick3DModel *>(QStringLiteral("model3"));
++ QVERIFY(coneModel);
++
++ // just left of the cone (model3)
++ auto result = view3d->pick(halfWidth - 11, horizontalPickLine);
++ QCOMPARE(result.objectHit(), nullptr);
++
++ // center top of the cone (model3)
++ result = view3d->pick(halfWidth, horizontalPickLine);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), coneModel);
++
++ // just right of the cone (model3)
++ result = view3d->pick(halfWidth + 11, horizontalPickLine);
++ QCOMPARE(result.objectHit(), nullptr);
++ }
++
++ // Necessary for doing comparisons of normalized vectors
++ static bool fuzzyCompareVectors(const QVector3D& v1, const QVector3D& v2, float epsilon = 0.000001f) {
++ return std::abs(v1.x() - v2.x()) < epsilon &&
++ std::abs(v1.y() - v2.y()) < epsilon &&
++ std::abs(v1.z() - v2.z()) < epsilon;
++
++ }
++
++ void tst_Picking::test_item_picking()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("item_picking.qml"), QSize(400, 400)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++ QQuick3DModel *model2 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model2"));
++ QVERIFY(model2);
++ QQuickItem *item2dPlusZ = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dPlusZNode", "item2dPlusZ"));
++ QVERIFY(item2dPlusZ);
++ QQuickItem *item2dMinusZ = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dMinusZNode", "item2dMinusZ"));
++ QVERIFY(item2dMinusZ);
++ QQuickItem *item2dPlusX = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dPlusXNode", "item2dPlusX"));
++ QVERIFY(item2dPlusX);
++ QQuickItem *item2dMinusX = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dMinusXNode", "item2dMinusX"));
++ QVERIFY(item2dMinusX);
++ QQuickItem *item2dPlusY = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dPlusYNode", "item2dPlusY"));
++ QVERIFY(item2dPlusY);
++ QQuickItem *item2dMinusY = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dMinusYNode", "item2dMinusY"));
++ QVERIFY(item2dMinusY);
++
++ QQuickItem *item2dRotation = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dRotationNode", "item2dRotation"));
++ QVERIFY(item2dRotation);
++ QQuickItem *item2dTranslation = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dTranslationNode", "item2dTranslation"));
++ QVERIFY(item2dTranslation);
++ QQuickItem *item2dScale = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dScaleNode", "item2dScale"));
++ QVERIFY(item2dScale);
++ QQuickItem *item2dComplex = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dComplexNode", "item2dComplex"));
++ QVERIFY(item2dComplex);
++ QQuickItem *item2dMadness = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dMadnessNode", "item2dMadness"));
++ QVERIFY(item2dMadness);
++
++
++ // These tests intentionally try to cast rays into nothing, to check that item2ds picking stays within the bounds of the items
++ // This is necessary since items2ds uses plane intersection for ray collision tests
++ // Z Axes
++ auto origin = QVector3D(200, 200, 200);
++ auto direction = QVector3D(0, 0, -1);
++ auto result = view3d->rayPick(origin, direction);
++ auto resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ origin = QVector3D(200, 200, -200);
++ direction = QVector3D(0, 0, -1);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // X Axes
++ origin = QVector3D(200, 200, 200);
++ direction = QVector3D(-1, 0, 0);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ origin = QVector3D(-200, 200, 200);
++ direction = QVector3D(1, 0, 0);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // Y Axes
++ origin = QVector3D(200, 200, 200);
++ direction = QVector3D(0, -1, 0);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ origin = QVector3D(200, -200, 200);
++ direction = QVector3D(0, 1, 0);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // These tests all cast a ray down from above
++ direction = QVector3D (0, -1, 0);
++ // Rotation
++ origin = QVector3D(250, 100, -50);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // Translation
++ origin = QVector3D(450, 100, 10);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // Scale
++ origin = QVector3D(700, 100, 110);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // Complex
++ origin = QVector3D(900, 100, 110);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // Madness (fake 3D transforms in 2D)
++ origin = QVector3D(1150, 100, 90);
++ result = view3d->rayPick(origin, direction);
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++ QVERIFY(resultsList.isEmpty());
++
++ // The following tests try to hit the item2ds
++ // NB: Only a front-face hit of an item2d should count as a hit
++
++ // +Z
++ origin = QVector3D(25, 25, 200);
++ direction = QVector3D(0, 0, -1);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dPlusZ);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 0, 1)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 3);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dPlusZ);
++ QCOMPARE(resultsList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[1].objectHit(), model1);
++ QCOMPARE(resultsList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[2].objectHit(), model2);
++
++ // -Z
++ // NB: In this case the distance of collision should be the exact same for both
++ // model2 and item2dMinusZ. Item2D collision always occurs first, so it should
++ // be the first in the list.
++ origin = QVector3D(25, 25, -200);
++ direction = QVector3D(0, 0, 1);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dMinusZ);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 0, -1)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 3);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dMinusZ);
++ QCOMPARE(resultsList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[1].objectHit(), model2);
++ QCOMPARE(resultsList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[2].objectHit(), model1);
++ QCOMPARE(resultsList[0].distance(), resultsList[1].distance());
++
++ // +X
++ // NB: in this case the distance of collision should be the exact same for both
++ // model2 and item2dPlusX. Item2D collision always occurs first, so it should
++ // be the first in the list.
++ origin = QVector3D(200, 25, -25);
++ direction = QVector3D(-1, 0, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dPlusX);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(1, 0, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 3);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dPlusX);
++ QCOMPARE(resultsList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[1].objectHit(), model2);
++ QCOMPARE(resultsList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[2].objectHit(), model1);
++ QCOMPARE(resultsList[0].distance(), resultsList[1].distance());
++
++ // -X
++ origin = QVector3D(-200, 25, -25);
++ direction = QVector3D(1, 0, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dMinusX);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(-1, 0, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 3);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dMinusX);
++ QCOMPARE(resultsList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[1].objectHit(), model1);
++ QCOMPARE(resultsList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[2].objectHit(), model2);
++
++ // +Y
++ // NB: in this case the distance of collision should be the exact same for both
++ // model2 and item2dPlusY. Item2D collision always occurs first, so it should
++ // be the first in the list.
++ origin = QVector3D(25, 200, -25);
++ direction = QVector3D(0, -1, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dPlusY);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 3);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dPlusY);
++ QCOMPARE(resultsList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[1].objectHit(), model2);
++ QCOMPARE(resultsList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[2].objectHit(), model1);
++ QCOMPARE(resultsList[0].distance(), resultsList[1].distance());
++
++ // -Y
++ origin = QVector3D(25, -200, -25);
++ direction = QVector3D(0, 1, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dMinusY);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, -1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 3);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dMinusY);
++ QCOMPARE(resultsList[1].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[1].objectHit(), model1);
++ QCOMPARE(resultsList[2].hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(resultsList[2].objectHit(), model2);
++
++
++ // The transform tests are aligned to the XZ plane, so all directions
++ // will be down the Y axis
++ direction = QVector3D(0, -1, 0);
++
++ // Rotation
++ origin = QVector3D(300, 100, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dRotation);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 1);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dRotation);
++
++ // Translate
++ origin = QVector3D(450, 100, -50);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dTranslation);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 1);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dTranslation);
++
++ // Scale
++ origin = QVector3D(700, 100, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dScale);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 1);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dScale);
++
++ // Complex
++ origin = QVector3D(900, 100, 0);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dComplex);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 1);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dComplex);
++
++ // Madness
++ origin = QVector3D(1150, 100, 50);
++ result = view3d->rayPick(origin, direction);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(result.itemHit(), item2dMadness);
++ QVERIFY(fuzzyCompareVectors(result.sceneNormal(), QVector3D(0, 1, 0)));
++ resultsList = view3d->rayPickAll(origin, direction);
++ QCOMPARE(resultsList.size(), 1);
++ QCOMPARE(resultsList[0].hitType(), QQuick3DPickResultEnums::HitType::Item);
++ QCOMPARE(resultsList[0].itemHit(), item2dMadness);
++
++ }
++
++
++ // When triangles become so small the picking algorithm fails because of floating point
++ // precision. This test generates a quad with count * count subdivisions leading to smaller
++ // and smaller triangles to pick against. Even if a mesh is very dense, the triangle intersection
++ // code should still work and return model1 as a hit.
++ void tst_Picking::test_picking_QTBUG_111997()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("qtbug_111997.qml"), QSize(100, 100)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++
++ const auto viewSize = view->size();
++ const float halfWidth = viewSize.width() * 0.5f;
++ const float halfHeight = viewSize.height() * 0.5f;
++
++ // Add custom geometry to the scene
++ QScopedPointer<CustomGeometry> geometry(new CustomGeometry(20));
++ geometry->setParent(model1);
++ geometry->setParentItem(model1);
++ model1->setGeometry(geometry.data());
++
++ // Check picking with 20 subdivisions
++ QVERIFY(waitForFrames(view.data(), 2));
++ auto result = view3d->pick(halfWidth, halfHeight);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++
++ // check picking with 350 subdivisions
++ geometry->setCount(350);
++ QVERIFY(waitForFrames(view.data(), 2));
++ result = view3d->pick(halfWidth, halfHeight);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++
++ // check picking with 500 subdivisions
++ geometry->setCount(500);
++ QVERIFY(waitForFrames(view.data(), 2));
++ result = view3d->pick(halfWidth, halfHeight);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ }
++
++ void tst_Picking::test_sphere_geometry()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("pickable_sphere.qml"), QSize(100, 100)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++
++ const auto viewSize = view->size();
++ const float halfWidth = viewSize.width() * 0.5f;
++ const float halfHeight = viewSize.height() * 0.5f;
++
++ // Wait for a couple of frames (so that geometry is up-to-date)
++ QVERIFY(waitForFrames(view.data(), 2));
++
++ auto result = view3d->pick(halfWidth, halfHeight);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ }
++
++ void tst_Picking::test_picking_corner_case()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("corner_case.qml"), QSize(400, 400)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++
++ const QVector3D direction = QVector3D(0.0f, 0.0f, 1.0f);
++
++ {
++ // bottom right corner (vertex)
++ const QVector3D origin1 = QVector3D(0.0f, 0.0f, -101.0f);
++ const QVector3D origin2 = QVector3D(0.0f, 0.0f, -100.0f);
++ auto result = view3d->rayPick(origin1, direction);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.scenePosition(), origin2);
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.uvPosition(), QVector2D(1.0f, 0.0f));
++
++ // The ray doesn't intersect the triangle as the origin
++ // starts on the triangle
++
++ // The ray intersects with the other face of the cube,
++ // but unless we test for backfaces the test should still
++
++ result = view3d->rayPick(origin2, direction);
++ QVERIFY(result.objectHit() == nullptr);
++ }
++
++ {
++ // top right corner (vertex)
++ const QVector3D origin1 = QVector3D(0.0f, 100.0f, -101.0f);
++ const QVector3D origin2 = QVector3D(0.0f, 100.0f, -100.0f);
++ auto result = view3d->rayPick(origin1, direction);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.scenePosition(), origin2);
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.uvPosition(), QVector2D(1.0f, 1.0f));
++
++ // The ray doesn't intersect the triangle as the origin
++ // starts on the triangle
++
++ // The ray intersects with the other face of the cube,
++ // but unless we test for backfaces the test should still
++
++ result = view3d->rayPick(origin2, direction);
++ QVERIFY(result.objectHit() == nullptr);
++ }
++
++ {
++ // bottom left corner (vertex)
++ const QVector3D origin1 = QVector3D(100.0f, 0.0f, -101.0f);
++ const QVector3D origin2 = QVector3D(100.0f, 0.0f, -100.0f);
++ auto result = view3d->rayPick(origin1, direction);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.scenePosition(), origin2);
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.uvPosition(), QVector2D(0.0f, 0.0f));
++
++ // The ray doesn't intersect the triangle as the origin
++ // starts on the triangle
++
++ // The ray intersects with the other face of the cube,
++ // but unless we test for backfaces the test should still
++
++ result = view3d->rayPick(origin2, direction);
++ QVERIFY(result.objectHit() == nullptr);
++ }
++
++ {
++ // top left corner (vertex)
++ const QVector3D origin1 = QVector3D(100.0f, 100.0f, -101.0f);
++ const QVector3D origin2 = QVector3D(100.0f, 100.0f, -100.0f);
++ auto result = view3d->rayPick(origin1, direction);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.scenePosition(), origin2);
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.uvPosition(), QVector2D(0.0f, 1.0f));
++
++ // The ray doesn't intersect the triangle as the origin
++ // starts on the triangle
++
++ // The ray intersects with the other face of the cube,
++ // but unless we test for backfaces the test should still
++
++ result = view3d->rayPick(origin2, direction);
++ QVERIFY(result.objectHit() == nullptr);
++ }
++
++ {
++ // Center
++ const QVector3D origin1 = QVector3D(50.0f, 50.0f, -101.0f);
++ const QVector3D origin2 = QVector3D(50.0f, 50.0f, -100.0f);
++ auto result = view3d->rayPick(origin1, direction);
++ QVERIFY(result.objectHit() != nullptr);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.scenePosition(), origin2);
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, -1.0f));
++ QCOMPARE(result.uvPosition(), QVector2D(0.5f, 0.5f));
++
++ // The ray doesn't intersect the triangle as the origin
++ // starts on the triangle
++
++ // The ray intersects with the other face of the cube,
++ // but unless we test for backfaces the test should still
++
++ result = view3d->rayPick(origin2, direction);
++ QVERIFY(result.objectHit() == nullptr);
++ }
++
++ }
++
++ void tst_Picking::test_triangleIntersect()
++ {
++ const QVector3D direction(0, 0, -1);
++ const QVector3D expectedNormal(0, 0, 1);
++ const QVector3D vert0(0, 0, 0);
++ const QVector3D vert1(-1, -1, 0);
++ const QVector3D vert2(1, -1, 0);
++
++ QVector3D normal;
++ float u;
++ float v;
++ bool intersects;
++ const QVector3D origin1(0, 0, 2);
++
++ const QSSGRenderRay ray1(origin1, direction); // intersection vert0
++
++ intersects = QSSGRenderRay::triangleIntersect(ray1,
++ vert0,
++ vert1,
++ vert2,
++ u,
++ v,
++ normal);
++ QVERIFY(intersects);
++ QCOMPARE(normal, expectedNormal);
++ QCOMPARE(u, 0.0f);
++ QCOMPARE(v, 0.0f);
++
++ const QVector3D origin2(-1, -1, 2);
++ const QSSGRenderRay ray2(origin2, direction); // intersection vert1
++
++ intersects = QSSGRenderRay::triangleIntersect(ray2,
++ vert0,
++ vert1,
++ vert2,
++ u,
++ v,
++ normal);
++ QVERIFY(intersects);
++ QCOMPARE(normal, expectedNormal);
++ QCOMPARE(u, 1.0f);
++ QCOMPARE(v, 0.0f);
++
++ const QVector3D origin3(1, -1, 2);
++ const QSSGRenderRay ray3(origin3, direction); // intersection vert2
++ intersects = QSSGRenderRay::triangleIntersect(ray3,
++ vert0,
++ vert1,
++ vert2,
++ u,
++ v,
++ normal);
++ QVERIFY(intersects);
++ QCOMPARE(normal, expectedNormal);
++ QCOMPARE(u, 0.0f);
++ QCOMPARE(v, 1.0f);
++ }
++
++ void tst_Picking::test_closest_point_picking()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("picking.qml"), QSize(400, 400)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++ QQuick3DModel *model1 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model1"));
++ QVERIFY(model1);
++ QQuick3DModel *model2 = view3d->findChild<QQuick3DModel *>(QStringLiteral("model2"));
++ QVERIFY(model2);
++ QQuick3DModel *instancedModel = view3d->findChild<QQuick3DModel *>(QStringLiteral("instancedModel"));
++ QVERIFY(instancedModel);
++ QQuickItem *item2d = qmlobject_cast<QQuickItem *>(find2DChildIn3DNode(view.data(), "item2dNode", "item2d"));
++ QVERIFY(item2d);
++
++ item2d->setEnabled(true);
++ item2d->setVisible(true);
++ QVERIFY(waitForFrames(view.data(), 1));
++
++ // model1 is a unit cube at (0,0,0), so it has a surface point at (0, 0, 50)
++
++ QVector3D origin;
++
++ // Model outside radius
++ origin = QVector3D(0.0f, 0.0f, 70.0f);
++ auto result = view3d->closestPointPick(origin, 10);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Null);
++
++ result = view3d->closestPointPick(origin, 30);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.distance(), 20.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.5f, 0.5f));
++ QCOMPARE(result.scenePosition(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(result.position(), QVector3D(0.0f, 0.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Test hit on edge. It's not specified which face it should choose, so ignore
++ // normals and UVs. model1 has an edge that passes through (-50, -50, 0)
++
++ origin = QVector3D(-70.0f, -70.0f, 0.0f); // distance == sqrt(20^2 + 20^2) < 30
++ result = view3d->closestPointPick(origin, 30, model1);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model1);
++ QCOMPARE(result.scenePosition(), QVector3D(-50.0f, -50.0f, 0.0f));
++ QVERIFY(qFuzzyCompare(result.distance(), float(sqrt(800.0f))));
++
++ // model2 is a unit cube at (50, 50, -50), so it has a surface point at (10, 10, 0)
++ // Check to see that we return the specified model, even if model1 is closer
++ origin = QVector3D(10.0f, 10.0f, 70.0f);
++ result = view3d->closestPointPick(origin, 100, model2);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), model2);
++ QCOMPARE(result.distance(), 70.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.1f, 0.1f));
++ QCOMPARE(result.scenePosition(), QVector3D(10.0f, 10.0f, 0.0f));
++ QCOMPARE(result.position(), QVector3D(-40.0f, -40.0f, 50.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.normal(), QVector3D(0.0f, 0.0f, 1.0f));
++ QCOMPARE(result.instanceIndex(), 0);
++ QCOMPARE(result.itemHit(), nullptr);
++
++ // Test instanced picking
++ // Instance index 2 describes a unit cube at (200, 0, 0), so it has a surface
++ // point at (150, 0, 0) with a normal of (-1, 0, 0)
++ origin = QVector3D(140.0f, 0.0f, 0.0f);
++ result = view3d->closestPointPick(origin, 20);
++ QCOMPARE(result.hitType(), QQuick3DPickResultEnums::HitType::Model);
++ QCOMPARE(result.objectHit(), instancedModel);
++ QCOMPARE(result.distance(), 10.0f);
++ QCOMPARE(result.uvPosition(), QVector2D(0.5f, 0.5f));
++ QCOMPARE(result.scenePosition(), QVector3D(150.0f, 0.0f, 0.0f));
++ QCOMPARE(result.position(), QVector3D(-50.0f, 0.0f, 0.0f));
++ QCOMPARE(result.sceneNormal(), QVector3D(-1.0f, 0.0f, 0.0f));
++ QCOMPARE(result.normal(), QVector3D(-1.0f, 0.0f, 0.0f));
++ QCOMPARE(result.instanceIndex(), 2);
++ QCOMPARE(result.itemHit(), nullptr);
++ }
++
++ void tst_Picking::test_triangleIntersect_cullMode_data()
++ {
++ QTest::addColumn<QVector3D>("origin");
++ QTest::addColumn<QVector3D>("direction");
++ QTest::addColumn<QSSGCullFaceMode>("cullMode");
++ QTest::addColumn<bool>("expectedIntersects");
++
++ // The triangle below faces +Z
++ const QVector3D frontOrigin(0.0f, -0.5f, 2.0f);
++ const QVector3D frontDirection(0.0f, 0.0f, -1.0f);
++ const QVector3D backOrigin(0.0f, -0.5f, -2.0f);
++ const QVector3D backDirection(0.0f, 0.0f, 1.0f);
++
++ QTest::newRow("front_backFaceCulling") << frontOrigin << frontDirection << QSSGCullFaceMode::Back << true;
++ QTest::newRow("front_frontFaceCulling") << frontOrigin << frontDirection << QSSGCullFaceMode::Front << false;
++ QTest::newRow("front_noCulling") << frontOrigin << frontDirection << QSSGCullFaceMode::Disabled << true;
++ QTest::newRow("front_frontAndBackCulling") << frontOrigin << frontDirection << QSSGCullFaceMode::FrontAndBack << false;
++
++ QTest::newRow("back_backFaceCulling") << backOrigin << backDirection << QSSGCullFaceMode::Back << false;
++ QTest::newRow("back_frontFaceCulling") << backOrigin << backDirection << QSSGCullFaceMode::Front << true;
++ QTest::newRow("back_noCulling") << backOrigin << backDirection << QSSGCullFaceMode::Disabled << true;
++ QTest::newRow("back_frontAndBackCulling") << backOrigin << backDirection << QSSGCullFaceMode::FrontAndBack << false;
++ }
++
++ void tst_Picking::test_triangleIntersect_cullMode()
++ {
++ QFETCH(QVector3D, origin);
++ QFETCH(QVector3D, direction);
++ QFETCH(QSSGCullFaceMode, cullMode);
++ QFETCH(bool, expectedIntersects);
++
++ const QVector3D vert0(0, 0, 0);
++ const QVector3D vert1(-1, -1, 0);
++ const QVector3D vert2(1, -1, 0);
++
++ QVector3D normal;
++ float u = 0.0f;
++ float v = 0.0f;
++
++ const QSSGRenderRay ray(origin, direction);
++ const bool intersects = QSSGRenderRay::triangleIntersect(ray, vert0, vert1, vert2, u, v, normal, cullMode);
++ QCOMPARE(intersects, expectedIntersects);
++
++ if (!intersects)
++ return;
++
++ QVERIFY(u >= 0.0f && u <= 1.0f);
++ QVERIFY(v >= 0.0f && v <= 1.0f);
++ QVERIFY(u + v <= 1.0f);
++ QCOMPARE(normal, QVector3D(0.0f, 0.0f, 1.0f));
++ }
++
++ void tst_Picking::test_picking_cull_mode()
++ {
++ QScopedPointer<QQuickView> view(createView(QLatin1String("cull_mode.qml"), QSize(600, 400)));
++ QVERIFY(view);
++ QVERIFY(QTest::qWaitForWindowExposed(view.data()));
++
++ QQuick3DViewport *view3d = view->findChild<QQuick3DViewport *>(QStringLiteral("view"));
++ QVERIFY(view3d);
++
++ const QImage rendered = grab(view.data());
++ if (rendered.isNull())
++ return; // was QFAIL'ed already
++
++ const qreal dpr = view->devicePixelRatio();
++
++ struct CullModeCase
++ {
++ const char *objectName;
++ QPoint viewportPos;
++ QColor renderedColor; // The clear color when the visible side is culled away
++ bool pickable;
++ };
++
++ // Whatever is culled away when rendering must not be pickable, and whatever is
++ // rendered must be.
++ static const CullModeCase cases[] = {
++ { "backCull", QPoint(100, 100), QColor(Qt::black), false },
++ { "frontCull", QPoint(300, 100), QColor(0, 255, 0), true },
++ { "noCull", QPoint(500, 100), QColor(0, 0, 255), true },
++ { "mirrorBackCull", QPoint(100, 300), QColor(Qt::black), false },
++ { "mirrorFrontCull", QPoint(300, 300), QColor(0, 255, 255), true },
++ { "mirrorNoCull", QPoint(500, 300), QColor(255, 0, 255), true },
++ };
++
++ for (const CullModeCase &c : cases) {
++ QQuick3DModel *model = view3d->findChild<QQuick3DModel *>(QLatin1String(c.objectName));
++ QVERIFY2(model, c.objectName);
++ QVERIFY2(comparePixel(rendered, c.viewportPos, dpr, c.renderedColor, 5), c.objectName);
++
++ const QQuick3DPickResult result = view3d->pick(c.viewportPos.x(), c.viewportPos.y());
++ if (c.pickable)
++ QVERIFY2(result.objectHit() == model, c.objectName);
++ else
++ QVERIFY2(result.objectHit() == nullptr, c.objectName);
++ }
++ }
++
++ QTEST_MAIN(tst_Picking)
++ #include "tst_picking.moc"