Support selectionQueryPosition and graphPositionQuery

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ää <tomi.korpipaa@qt.io>
This commit is contained in:
Sakaria Pouke 2025-08-14 12:45:19 +03:00 committed by Tomi Korpipaa
parent f84d80120f
commit 47dc05aac6
5 changed files with 284 additions and 24 deletions

View File

@ -821,7 +821,11 @@ QQuickGraphsItem::QQuickGraphsItem(QQuickItem *parent)
connect(m_scene, connect(m_scene,
&Q3DScene::graphPositionQueryChanged, &Q3DScene::graphPositionQueryChanged,
this, this,
&QQuickGraphsItem::handleQueryPositionChanged); &QQuickGraphsItem::handleGraphQueryPositionChanged);
connect(m_scene,
&Q3DScene::selectionQueryPositionChanged,
this,
&QQuickGraphsItem::handleSelectionQueryPositionChanged);
connect(m_scene, &Q3DScene::primarySubViewportChanged, connect(m_scene, &Q3DScene::primarySubViewportChanged,
this, this,
&QQuickGraphsItem::handlePrimarySubViewportChanged); &QQuickGraphsItem::handlePrimarySubViewportChanged);
@ -1070,7 +1074,7 @@ void QQuickGraphsItem::handleRequestShadowQuality(QtGraphs3D::ShadowQuality qual
setShadowQuality(quality); setShadowQuality(quality);
} }
void QQuickGraphsItem::handleQueryPositionChanged(QPoint position) void QQuickGraphsItem::handleGraphQueryPositionChanged(QPoint position)
{ {
QVector3D data = graphPositionAt(position); QVector3D data = graphPositionAt(position);
setGraphPositionQueryPending(false); setGraphPositionQueryPending(false);
@ -1078,6 +1082,11 @@ void QQuickGraphsItem::handleQueryPositionChanged(QPoint position)
emit queriedGraphPositionChanged(data); emit queriedGraphPositionChanged(data);
} }
void QQuickGraphsItem::handleSelectionQueryPositionChanged(QPoint position)
{
doPicking(position);
}
void QQuickGraphsItem::handlePrimarySubViewportChanged(const QRect rect) void QQuickGraphsItem::handlePrimarySubViewportChanged(const QRect rect)
{ {
m_primarySubView = rect; m_primarySubView = rect;

View File

@ -663,7 +663,8 @@ protected:
virtual void createSliceView(); virtual void createSliceView();
QQuick3DViewport *createOffscreenSliceView(QtGraphs3D::SliceCaptureType sliceType); QQuick3DViewport *createOffscreenSliceView(QtGraphs3D::SliceCaptureType sliceType);
void handleQueryPositionChanged(QPoint position); void handleGraphQueryPositionChanged(QPoint position);
void handleSelectionQueryPositionChanged(QPoint position);
void handlePrimarySubViewportChanged(const QRect rect); void handlePrimarySubViewportChanged(const QRect rect);
void handleSecondarySubViewportChanged(const QRect rect); void handleSecondarySubViewportChanged(const QRect rect);

View File

@ -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"
}
}

View File

@ -17,14 +17,18 @@ int main(int argc, char **argv)
tst_qmltest skip; tst_qmltest skip;
return QTest::qExec(&skip, argc, argv); return QTest::qExec(&skip, argc, argv);
} }
#ifdef Q_OS_QNX
if (qEnvironmentVariable("QTEST_ENVIRONMENT").split(' ').contains("ci") && if (qEnvironmentVariable("QTEST_ENVIRONMENT").split(' ').contains("ci") &&
qEnvironmentVariable("QT_QPA_PLATFORM").split(' ').contains("offscreen") 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; tst_qmltest skip;
return QTest::qExec(&skip, argc, argv); 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 #endif
QTEST_SET_MAIN_SOURCE_PATH QTEST_SET_MAIN_SOURCE_PATH
return quick_test_main(argc, argv, "qmltest", QUICK_TEST_SOURCE_DIR); return quick_test_main(argc, argv, "qmltest", QUICK_TEST_SOURCE_DIR);

View File

@ -14,6 +14,7 @@ Item {
property real maxSegmentCount: 10 property real maxSegmentCount: 10
property real minSegmentCount: 1 property real minSegmentCount: 1
property var activeGraph: scatterGraph
Data { Data {
id: graphData id: graphData
@ -25,8 +26,97 @@ Item {
width: parent.width width: parent.width
height: parent.height - buttonLayout.height 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 { Scatter3D {
id: scatterGraph id: scatterGraph
visible: activeGraph == scatterGraph
width: dataView.width width: dataView.width
height: dataView.height height: dataView.height
theme: GraphsTheme { theme: GraphsTheme.Theme.QtGreen } theme: GraphsTheme { theme: GraphsTheme.Theme.QtGreen }
@ -88,7 +178,7 @@ Item {
onWheel: (wheel)=> { onWheel: (wheel)=> {
// Adjust zoom level based on what zoom range we're in. // Adjust zoom level based on what zoom range we're in.
var zoomLevel = scatterGraph.zoomLevel; var zoomLevel = activeGraph.zoomLevel;
if (zoomLevel > 100) if (zoomLevel > 100)
zoomLevel += wheel.angleDelta.y / 12.0; zoomLevel += wheel.angleDelta.y / 12.0;
else if (zoomLevel > 50) else if (zoomLevel > 50)
@ -100,11 +190,11 @@ Item {
else if (zoomLevel < 10) else if (zoomLevel < 10)
zoomLevel = 10; zoomLevel = 10;
scatterGraph.zoomLevel = zoomLevel; activeGraph.zoomLevel = zoomLevel;
} }
onClicked: { onClicked: {
console.log("Queried at: " + Qt.point(mouseX, mouseY)) 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 running: true
repeat: true repeat: true
onTriggered: { onTriggered: {
scatterGraph.scene.selectionQueryPosition = Qt.point(-1, -1); activeGraph.scene.selectionQueryPosition = Qt.point(-1, -1);
scatterGraph.scene.selectionQueryPosition = Qt.point(inputArea.mouseX, inputArea.mouseY); activeGraph.scene.selectionQueryPosition = Qt.point(inputArea.mouseX, inputArea.mouseY);
} }
} }
} }
NumberAnimation { NumberAnimation {
id: cameraAnimationX id: scatterCameraAnimationX
loops: Animation.Infinite loops: Animation.Infinite
running: true running: true
target: scatterGraph target: scatterGraph
@ -130,9 +220,19 @@ Item {
to: 360.0 to: 360.0
duration: 20000 duration: 20000
} }
NumberAnimation {
id: surfaceCameraAnimationX
loops: Animation.Infinite
running: true
target: surfaceGraph
property:"cameraXRotation"
from: 0.0
to: 360.0
duration: 20000
}
SequentialAnimation { SequentialAnimation {
id: cameraAnimationY id: scatterCameraAnimationY
loops: Animation.Infinite loops: Animation.Infinite
running: true running: true
@ -154,6 +254,29 @@ Item {
easing.type: Easing.InOutSine 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 { RowLayout {
id: buttonLayout id: buttonLayout
@ -165,14 +288,14 @@ Item {
Button { Button {
id: shadowToggle id: shadowToggle
Layout.fillHeight: true 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" text: "Hide Shadows"
onClicked: { onClicked: {
if (scatterGraph.shadowQuality === Graphs3D.ShadowQuality.None) { if (activeGraph.shadowQuality === Graphs3D.ShadowQuality.None) {
scatterGraph.shadowQuality = Graphs3D.ShadowQuality.Medium; activeGraph.shadowQuality = Graphs3D.ShadowQuality.Medium;
text = "Hide Shadows"; text = "Hide Shadows";
} else { } else {
scatterGraph.shadowQuality = Graphs3D.ShadowQuality.None; activeGraph.shadowQuality = Graphs3D.ShadowQuality.None;
text = "Show Shadows"; text = "Show Shadows";
} }
} }
@ -181,7 +304,7 @@ Item {
Button { Button {
id: cameraToggle id: cameraToggle
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumWidth: parent.width / 3 Layout.minimumWidth: parent.width / 4
text: "Pause Camera" text: "Pause Camera"
onClicked: { 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 { Button {
id: exitButton id: exitButton
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumWidth: parent.width / 3 Layout.minimumWidth: parent.width / 4
text: "Quit" text: "Quit"
onClicked: Qt.quit(); onClicked: Qt.quit();
} }
@ -224,7 +360,7 @@ Item {
to: mainView.maxSegmentCount to: mainView.maxSegmentCount
value: 5 value: 5
onValueChanged: scatterGraph.axisX.segmentCount = value onValueChanged: activeGraph.axisX.segmentCount = value
} }
Slider { Slider {
@ -233,7 +369,7 @@ Item {
to: mainView.maxSegmentCount to: mainView.maxSegmentCount
value: 1 value: 1
onValueChanged: scatterGraph.axisX.subSegmentCount = value onValueChanged: activeGraph.axisX.subSegmentCount = value
} }
} }
@ -250,7 +386,7 @@ Item {
to: mainView.maxSegmentCount to: mainView.maxSegmentCount
value: 5 value: 5
onValueChanged: scatterGraph.axisY.segmentCount = value onValueChanged: activeGraph.axisY.segmentCount = value
} }
Slider { Slider {
@ -259,7 +395,7 @@ Item {
to: mainView.maxSegmentCount to: mainView.maxSegmentCount
value: 1 value: 1
onValueChanged: scatterGraph.axisY.subSegmentCount = value onValueChanged: activeGraph.axisY.subSegmentCount = value
} }
} }
@ -275,7 +411,7 @@ Item {
to: mainView.maxSegmentCount to: mainView.maxSegmentCount
value: 5 value: 5
onValueChanged: scatterGraph.axisZ.segmentCount = value onValueChanged: activeGraph.axisZ.segmentCount = value
} }
Slider { Slider {
@ -284,7 +420,7 @@ Item {
to: mainView.maxSegmentCount to: mainView.maxSegmentCount
value: 1 value: 1
onValueChanged: scatterGraph.axisZ.subSegmentCount = value onValueChanged: activeGraph.axisZ.subSegmentCount = value
} }
} }
} }