From 47dc05aac64147fb823de33191781a5596a9f233 Mon Sep 17 00:00:00 2001 From: Sakaria Pouke Date: Thu, 14 Aug 2025 12:45:19 +0300 Subject: [PATCH] Support selectionQueryPosition and graphPositionQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit selectionQuery is essentially a legacy way to do picking while graphPositionQuery returns a picked position of the graph. Task-number: QTBUG-138828 Pick-to: 6.10 6.8 Change-Id: Ieb90086e0416558bb31633a2b28db37443dd6362 Reviewed-by: Tomi Korpipää --- src/graphs3d/qml/qquickgraphsitem.cpp | 13 +- src/graphs3d/qml/qquickgraphsitem_p.h | 3 +- tests/auto/qmltest/scene3d/tst_query.qml | 110 +++++++++++ tests/auto/qmltest/tst_qmltest.cpp | 8 +- .../qml/qmlcustominput/main.qml | 174 ++++++++++++++++-- 5 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 tests/auto/qmltest/scene3d/tst_query.qml diff --git a/src/graphs3d/qml/qquickgraphsitem.cpp b/src/graphs3d/qml/qquickgraphsitem.cpp index 76d5d159..50e1071e 100644 --- a/src/graphs3d/qml/qquickgraphsitem.cpp +++ b/src/graphs3d/qml/qquickgraphsitem.cpp @@ -821,7 +821,11 @@ QQuickGraphsItem::QQuickGraphsItem(QQuickItem *parent) connect(m_scene, &Q3DScene::graphPositionQueryChanged, this, - &QQuickGraphsItem::handleQueryPositionChanged); + &QQuickGraphsItem::handleGraphQueryPositionChanged); + connect(m_scene, + &Q3DScene::selectionQueryPositionChanged, + this, + &QQuickGraphsItem::handleSelectionQueryPositionChanged); connect(m_scene, &Q3DScene::primarySubViewportChanged, this, &QQuickGraphsItem::handlePrimarySubViewportChanged); @@ -1070,7 +1074,7 @@ void QQuickGraphsItem::handleRequestShadowQuality(QtGraphs3D::ShadowQuality qual setShadowQuality(quality); } -void QQuickGraphsItem::handleQueryPositionChanged(QPoint position) +void QQuickGraphsItem::handleGraphQueryPositionChanged(QPoint position) { QVector3D data = graphPositionAt(position); setGraphPositionQueryPending(false); @@ -1078,6 +1082,11 @@ void QQuickGraphsItem::handleQueryPositionChanged(QPoint position) emit queriedGraphPositionChanged(data); } +void QQuickGraphsItem::handleSelectionQueryPositionChanged(QPoint position) +{ + doPicking(position); +} + void QQuickGraphsItem::handlePrimarySubViewportChanged(const QRect rect) { m_primarySubView = rect; diff --git a/src/graphs3d/qml/qquickgraphsitem_p.h b/src/graphs3d/qml/qquickgraphsitem_p.h index 75c8324e..9b9f0806 100644 --- a/src/graphs3d/qml/qquickgraphsitem_p.h +++ b/src/graphs3d/qml/qquickgraphsitem_p.h @@ -663,7 +663,8 @@ protected: virtual void createSliceView(); QQuick3DViewport *createOffscreenSliceView(QtGraphs3D::SliceCaptureType sliceType); - void handleQueryPositionChanged(QPoint position); + void handleGraphQueryPositionChanged(QPoint position); + void handleSelectionQueryPositionChanged(QPoint position); void handlePrimarySubViewportChanged(const QRect rect); void handleSecondarySubViewportChanged(const QRect rect); diff --git a/tests/auto/qmltest/scene3d/tst_query.qml b/tests/auto/qmltest/scene3d/tst_query.qml new file mode 100644 index 00000000..41dc861f --- /dev/null +++ b/tests/auto/qmltest/scene3d/tst_query.qml @@ -0,0 +1,110 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtGraphs +import QtTest 1.1 + +Item { + id: top + height: 150 + width: 150 + + Scatter3D { + id: invalid + height: 150 + width: 150 + Scatter3DSeries { + ItemModelScatterDataProxy { + itemModel: ListModel { + ListElement {x:"0"; y:"0"; z:"0"} + ListElement {x:"0"; y:"0"; z:"1"} + ListElement {x:"1"; y:"0"; z:"0"} + ListElement {x:"1"; y:"0"; z:"1"} + } + xPosRole: "x" + yPosRole: "y" + zPosRole: "z" + } + itemSize: 1.0 + } + } + + Scatter3D { + id: zero + anchors.fill: parent + Scatter3DSeries { + ItemModelScatterDataProxy { + itemModel: ListModel { + ListElement {x:"0"; y:"0"; z:"0"} + ListElement {x:"0"; y:"0"; z:"1"} + ListElement {x:"1"; y:"0"; z:"0"} + ListElement {x:"1"; y:"0"; z:"1"} + } + xPosRole: "x" + yPosRole: "y" + zPosRole: "z" + } + itemSize: 1.0 + } + } + + Scatter3D { + id: valid + height: 150 + width: 150 + property int middleX: width * 0.5 + property int middleY: height * 0.5 + measureFps: true // force rendering + + Scatter3DSeries { + ItemModelScatterDataProxy { + itemModel: ListModel { + ListElement {xPos:"0"; yPos:"0"; zPos:"0"} + } + xPosRole: "xPos" + yPosRole: "yPos" + zPosRole: "zPos" + } + itemSize: 1.0 + } + } + + TestCase { + name: "Scene invalid query" + function test_invalid_query() { + waitForRendering(top) + compare(invalid.scene.graphPositionQuery, Qt.point(-1, -1)) + compare(invalid.queriedGraphPosition, Qt.vector3d(0, 0, 0)) + } + } + + TestCase { + name: "Scene zero query" + function test_zero_query() { + zero.scene.graphPositionQuery = Qt.point(0, 0) + waitForRendering(top) + compare(zero.scene.graphPositionQuery, Qt.point(0, 0)) + compare(zero.queriedGraphPosition, Qt.vector3d(0, 0, 0)) + } + } + + TestCase { + name: "Scene valid query" + when: valid.currentFps > 0 + function test_valid_query() { + valid.scene.graphPositionQuery = Qt.point(valid.middleX + 1, valid.middleY) + waitForPolish(top) + waitForRendering(top) + compare(graphPositionQuerySpy.count, 1) + compare(valid.scene.graphPositionQuery, Qt.point(valid.middleX + 1, valid.middleY)) + verify(valid.queriedGraphPosition.fuzzyEquals(Qt.vector3d(2.68778, 0, 233.333))) + } + } + + SignalSpy { + id: graphPositionQuerySpy + target: valid.scene + signalName: "graphPositionQueryChanged" + } +} diff --git a/tests/auto/qmltest/tst_qmltest.cpp b/tests/auto/qmltest/tst_qmltest.cpp index 47a79912..6b3dc589 100644 --- a/tests/auto/qmltest/tst_qmltest.cpp +++ b/tests/auto/qmltest/tst_qmltest.cpp @@ -17,14 +17,18 @@ int main(int argc, char **argv) tst_qmltest skip; return QTest::qExec(&skip, argc, argv); } -#ifdef Q_OS_QNX if (qEnvironmentVariable("QTEST_ENVIRONMENT").split(' ').contains("ci") && qEnvironmentVariable("QT_QPA_PLATFORM").split(' ').contains("offscreen") ) { - qWarning("This test would fail on CI QNX QEMU without OpenGL support, so it will be skipped."); + qWarning("This test would fail on CI on offscreen test targets, so it will be skipped."); tst_qmltest skip; return QTest::qExec(&skip, argc, argv); } +#ifdef Q_OS_VXWORKS + qWarning("This test would fail due to VxWorks QtQuick3D support shortcomings, so it will " + "be skipped."); + tst_qmltest skip; + return QTest::qExec(&skip, argc, argv); #endif QTEST_SET_MAIN_SOURCE_PATH return quick_test_main(argc, argv, "qmltest", QUICK_TEST_SOURCE_DIR); diff --git a/tests/manual/qmlcustominput/qml/qmlcustominput/main.qml b/tests/manual/qmlcustominput/qml/qmlcustominput/main.qml index eb256a8d..ab3f4f38 100644 --- a/tests/manual/qmlcustominput/qml/qmlcustominput/main.qml +++ b/tests/manual/qmlcustominput/qml/qmlcustominput/main.qml @@ -14,6 +14,7 @@ Item { property real maxSegmentCount: 10 property real minSegmentCount: 1 + property var activeGraph: scatterGraph Data { id: graphData @@ -25,8 +26,97 @@ Item { width: parent.width height: parent.height - buttonLayout.height + Surface3D { + id: surfaceGraph + width: dataView.width + height: dataView.height + visible: activeGraph == surfaceGraph + theme: GraphsTheme { theme: GraphsTheme.Theme.QtGreen } + shadowQuality: Graphs3D.ShadowQuality.Medium + cameraYRotation: 30.0 + + Surface3DSeries { + id: surfaceSeries + itemLabelFormat: "One - X:@xLabel Y:@yLabel Z:@zLabel" + mesh: Abstract3DSeries.Mesh.Cube + + + ItemModelSurfaceDataProxy { + itemModel : ListModel { + id: surfaceDataModel + ListElement{ coords: "0,0"; data: "20.0/10.0/4.75"; } + ListElement{ coords: "1,0"; data: "21.1/10.3/3.00"; } + ListElement{ coords: "2,0"; data: "22.5/10.7/1.24"; } + ListElement{ coords: "3,0"; data: "24.0/10.5/2.53"; } + ListElement{ coords: "0,1"; data: "20.2/11.2/3.55"; } + ListElement{ coords: "1,1"; data: "21.3/11.5/3.03"; } + ListElement{ coords: "2,1"; data: "22.6/11.7/3.46"; } + ListElement{ coords: "3,1"; data: "23.4/11.5/4.12"; } + ListElement{ coords: "0,2"; data: "20.2/12.3/3.37"; } + ListElement{ coords: "1,2"; data: "21.1/12.4/2.98"; } + ListElement{ coords: "2,2"; data: "22.5/12.1/3.33"; } + ListElement{ coords: "3,2"; data: "23.3/12.7/3.23"; } + ListElement{ coords: "0,3"; data: "20.7/13.3/5.34"; } + ListElement{ coords: "1,3"; data: "21.5/13.2/4.54"; } + ListElement{ coords: "2,3"; data: "22.4/13.6/4.65"; } + ListElement{ coords: "3,3"; data: "23.2/13.4/6.67"; } + ListElement{ coords: "0,4"; data: "20.6/15.0/6.01"; } + ListElement{ coords: "1,4"; data: "21.3/14.6/5.83"; } + ListElement{ coords: "2,4"; data: "22.5/14.8/7.32"; } + ListElement{ coords: "3,4"; data: "23.7/14.3/6.90"; } + + ListElement{ coords: "0,0"; data: "40.0/30.0/14.75"; } + ListElement{ coords: "1,0"; data: "41.1/30.3/13.00"; } + ListElement{ coords: "2,0"; data: "42.5/30.7/11.24"; } + ListElement{ coords: "3,0"; data: "44.0/30.5/12.53"; } + ListElement{ coords: "0,1"; data: "40.2/31.2/13.55"; } + ListElement{ coords: "1,1"; data: "41.3/31.5/13.03"; } + ListElement{ coords: "2,1"; data: "42.6/31.7/13.46"; } + ListElement{ coords: "3,1"; data: "43.4/31.5/14.12"; } + ListElement{ coords: "0,2"; data: "40.2/32.3/13.37"; } + ListElement{ coords: "1,2"; data: "41.1/32.4/12.98"; } + ListElement{ coords: "2,2"; data: "42.5/32.1/13.33"; } + ListElement{ coords: "3,2"; data: "43.3/32.7/13.23"; } + ListElement{ coords: "0,3"; data: "40.7/33.3/15.34"; } + ListElement{ coords: "1,3"; data: "41.5/33.2/14.54"; } + ListElement{ coords: "2,3"; data: "42.4/33.6/14.65"; } + ListElement{ coords: "3,3"; data: "43.2/33.4/16.67"; } + ListElement{ coords: "0,4"; data: "40.6/35.0/16.01"; } + ListElement{ coords: "1,4"; data: "41.3/34.6/15.83"; } + ListElement{ coords: "2,4"; data: "42.5/34.8/17.32"; } + ListElement{ coords: "3,4"; data: "43.7/34.3/16.90"; } + + } + + rowRole: "coords" + columnRole: "coords" + xPosRole: "data" + zPosRole: "data" + yPosRole: "data" + rowRolePattern: /(\d),\d/ + columnRolePattern: /(\d),(\d)/ + xPosRolePattern: /^([asd]*)([fgh]*)([jkl]*)[^\/]*\/([^\/]*)\/.*$/ + yPosRolePattern: /^([^\/]*)\/([^\/]*)\/(.*)$/ + zPosRolePattern: /^([asd]*)([qwe]*)([tyu]*)([fgj]*)([^\/]*)\/[^\/]*\/.*$/ + rowRoleReplace: "\\1" + columnRoleReplace: "\\2" + xPosRoleReplace: "\\4" + yPosRoleReplace: "\\3" + zPosRoleReplace: "\\5" + } + } + + Component.onCompleted: { + surfaceGraph.unsetDefaultInputHandler(); + } + + onQueriedGraphPositionChanged: + console.log("Queried Position : " + queriedGraphPosition) + } + Scatter3D { id: scatterGraph + visible: activeGraph == scatterGraph width: dataView.width height: dataView.height theme: GraphsTheme { theme: GraphsTheme.Theme.QtGreen } @@ -88,7 +178,7 @@ Item { onWheel: (wheel)=> { // Adjust zoom level based on what zoom range we're in. - var zoomLevel = scatterGraph.zoomLevel; + var zoomLevel = activeGraph.zoomLevel; if (zoomLevel > 100) zoomLevel += wheel.angleDelta.y / 12.0; else if (zoomLevel > 50) @@ -100,11 +190,11 @@ Item { else if (zoomLevel < 10) zoomLevel = 10; - scatterGraph.zoomLevel = zoomLevel; + activeGraph.zoomLevel = zoomLevel; } onClicked: { console.log("Queried at: " + Qt.point(mouseX, mouseY)) - scatterGraph.scene.graphPositionQuery = Qt.point(mouseX, mouseY) + activeGraph.scene.graphPositionQuery = Qt.point(mouseX, mouseY) } } @@ -114,14 +204,14 @@ Item { running: true repeat: true onTriggered: { - scatterGraph.scene.selectionQueryPosition = Qt.point(-1, -1); - scatterGraph.scene.selectionQueryPosition = Qt.point(inputArea.mouseX, inputArea.mouseY); + activeGraph.scene.selectionQueryPosition = Qt.point(-1, -1); + activeGraph.scene.selectionQueryPosition = Qt.point(inputArea.mouseX, inputArea.mouseY); } } } NumberAnimation { - id: cameraAnimationX + id: scatterCameraAnimationX loops: Animation.Infinite running: true target: scatterGraph @@ -130,9 +220,19 @@ Item { to: 360.0 duration: 20000 } + NumberAnimation { + id: surfaceCameraAnimationX + loops: Animation.Infinite + running: true + target: surfaceGraph + property:"cameraXRotation" + from: 0.0 + to: 360.0 + duration: 20000 + } SequentialAnimation { - id: cameraAnimationY + id: scatterCameraAnimationY loops: Animation.Infinite running: true @@ -154,6 +254,29 @@ Item { easing.type: Easing.InOutSine } } + SequentialAnimation { + id: surfaceCameraAnimationY + loops: Animation.Infinite + running: true + + NumberAnimation { + target: surfaceGraph + property:"cameraYRotation" + from: 5.0 + to: 45.0 + duration: 9000 + easing.type: Easing.InOutSine + } + + NumberAnimation { + target: surfaceGraph + property:"cameraYRotation" + from: 45.0 + to: 5.0 + duration: 9000 + easing.type: Easing.InOutSine + } + } RowLayout { id: buttonLayout @@ -165,14 +288,14 @@ Item { Button { id: shadowToggle Layout.fillHeight: true - Layout.minimumWidth: parent.width / 3 // 3 buttons divided equally in the layout + Layout.minimumWidth: parent.width / 4 // 4 buttons divided equally in the layout text: "Hide Shadows" onClicked: { - if (scatterGraph.shadowQuality === Graphs3D.ShadowQuality.None) { - scatterGraph.shadowQuality = Graphs3D.ShadowQuality.Medium; + if (activeGraph.shadowQuality === Graphs3D.ShadowQuality.None) { + activeGraph.shadowQuality = Graphs3D.ShadowQuality.Medium; text = "Hide Shadows"; } else { - scatterGraph.shadowQuality = Graphs3D.ShadowQuality.None; + activeGraph.shadowQuality = Graphs3D.ShadowQuality.None; text = "Show Shadows"; } } @@ -181,7 +304,7 @@ Item { Button { id: cameraToggle Layout.fillHeight: true - Layout.minimumWidth: parent.width / 3 + Layout.minimumWidth: parent.width / 4 text: "Pause Camera" onClicked: { @@ -195,10 +318,23 @@ Item { } } + Button { + id: graphToggle + Layout.fillHeight: true + Layout.minimumWidth: parent.width / 4 + text: "Switch graph type" + onClicked : { + if (activeGraph == scatterGraph) + activeGraph = surfaceGraph + else + activeGraph = scatterGraph + } + } + Button { id: exitButton Layout.fillHeight: true - Layout.minimumWidth: parent.width / 3 + Layout.minimumWidth: parent.width / 4 text: "Quit" onClicked: Qt.quit(); } @@ -224,7 +360,7 @@ Item { to: mainView.maxSegmentCount value: 5 - onValueChanged: scatterGraph.axisX.segmentCount = value + onValueChanged: activeGraph.axisX.segmentCount = value } Slider { @@ -233,7 +369,7 @@ Item { to: mainView.maxSegmentCount value: 1 - onValueChanged: scatterGraph.axisX.subSegmentCount = value + onValueChanged: activeGraph.axisX.subSegmentCount = value } } @@ -250,7 +386,7 @@ Item { to: mainView.maxSegmentCount value: 5 - onValueChanged: scatterGraph.axisY.segmentCount = value + onValueChanged: activeGraph.axisY.segmentCount = value } Slider { @@ -259,7 +395,7 @@ Item { to: mainView.maxSegmentCount value: 1 - onValueChanged: scatterGraph.axisY.subSegmentCount = value + onValueChanged: activeGraph.axisY.subSegmentCount = value } } @@ -275,7 +411,7 @@ Item { to: mainView.maxSegmentCount value: 5 - onValueChanged: scatterGraph.axisZ.segmentCount = value + onValueChanged: activeGraph.axisZ.segmentCount = value } Slider { @@ -284,7 +420,7 @@ Item { to: mainView.maxSegmentCount value: 1 - onValueChanged: scatterGraph.axisZ.subSegmentCount = value + onValueChanged: activeGraph.axisZ.subSegmentCount = value } } }