Axis Handling

 // Copyright (C) 2023 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

 import QtQuick
 import QtQuick.Controls
 import QtGraphs

 Item {
     id: axisDragView

     property int selectedAxisLabel: -1
     property real dragSpeedModifier: 100.0
     property int currentMouseX: -1
     property int currentMouseY: -1
     property int previousMouseX: -1
     property int previousMouseY: -1

     required property bool portraitMode

     ListModel {
         id: graphModel
         ListElement{ xPos: 0.0; yPos: 0.0; zPos: 0.0; rotation: "@0,0,0,0" }
         ListElement{ xPos: 1.0; yPos: 1.0; zPos: 1.0; rotation: "@45,1,1,1" }
     }

     Timer {
         id: dataTimer
         interval: 1
         running: true
         repeat: true
         property bool isIncreasing: true
         property real rotationAngle: 0

         function generateQuaternion() {
             return "@" + Math.random() * 360 + "," + Math.random() + ","
                     + Math.random() + "," + Math.random();
         }

         function appendRow() {
             graphModel.append({"xPos": Math.random(),
                                   "yPos": Math.random(),
                                   "zPos": Math.random(),
                                   "rotation": generateQuaternion()
                               });
         }

         onTriggered: {
             rotationAngle = rotationAngle + 1;
             qtCube.setRotationAxisAndAngle(Qt.vector3d(1, 0, 1), rotationAngle);
             scatterSeries.setMeshAxisAndAngle(Qt.vector3d(1, 1, 1), rotationAngle);
             if (isIncreasing) {
                 for (var i = 0; i < 10; i++)
                     appendRow();
                 if (graphModel.count > 2002) {
                     scatterGraph.theme = isabelleTheme;
                     isIncreasing = false;
                 }
             } else {
                 graphModel.remove(2, 10);
                 if (graphModel.count === 2) {
                     scatterGraph.theme = dynamicColorTheme;
                     isIncreasing = true;
                 }
             }
         }
     }

     Color {
         id: dynamicColor
         ColorAnimation on color {
             from: "red"
             to: "yellow"
             duration: 2000
             loops: Animation.Infinite
         }
     }

     Theme3D {
         id: dynamicColorTheme
         type: Theme3D.Theme.Ebony
         baseColors: [dynamicColor]
         font.pointSize: 50
         labelBorderEnabled: true
         labelBackgroundColor: "gold"
         labelTextColor: "black"
     }

     Theme3D {
         id: isabelleTheme
         type: Theme3D.Theme.Isabelle
         font.pointSize: 50
         labelBorderEnabled: true
         labelBackgroundColor: "gold"
         labelTextColor: "black"
     }

     Scatter3D {
         id: scatterGraph
         inputHandler: null
         anchors.fill: parent
         theme: dynamicColorTheme
         shadowQuality: AbstractGraph3D.ShadowQuality.Medium
         cameraYRotation: 45.0
         cameraXRotation: 45.0
         cameraZoomLevel: 75.0

         Scatter3DSeries {
             id: scatterSeries
             itemLabelFormat: "X:@xLabel Y:@yLabel Z:@zLabel"
             mesh: Abstract3DSeries.Mesh.Cube

             ItemModelScatterDataProxy {
                 itemModel: graphModel
                 xPosRole: "xPos"
                 yPosRole: "yPos"
                 zPosRole: "zPos"
                 rotationRole: "rotation"
             }
         }
         customItemList: [
             Custom3DItem {
                 id: qtCube
                 meshFile: ":/qml/axishandling/cube.mesh"
                 textureFile: ":/qml/axishandling/cubetexture.png"
                 position: Qt.vector3d(0.65, 0.35, 0.65)
                 scaling: Qt.vector3d(0.3, 0.3, 0.3)
             }
         ]
         onSelectedElementChanged: {
             if (selectedElement >= AbstractGraph3D.ElementType.AxisXLabel
                     && selectedElement <= AbstractGraph3D.ElementType.AxisZLabel) {
                 selectedAxisLabel = selectedElement;
             } else {
                 selectedAxisLabel = -1;
             }
         }
     }

     MouseArea {
         anchors.fill: parent
         hoverEnabled: true
         acceptedButtons: Qt.LeftButton

         onPositionChanged: (mouse)=> {
                                currentMouseX = mouse.x;
                                currentMouseY = mouse.y;
                                if (pressed && selectedAxisLabel != -1)
                                    axisDragView.dragAxis();
                                previousMouseX = currentMouseX;
                                previousMouseY = currentMouseY;
                            }

         onPressed: (mouse)=> {
                        scatterGraph.scene.selectionQueryPosition = Qt.point(mouse.x, mouse.y);
                    }

         onReleased: {
             // We need to clear mouse positions and selected axis, because touch devices cannot
             // track position all the time
             selectedAxisLabel = -1;
             currentMouseX = -1;
             currentMouseY = -1;
             previousMouseX = -1;
             previousMouseY = -1;
         }
     }

     function dragAxis() {
         // Do nothing if previous mouse position is uninitialized
         if (previousMouseX === -1)
             return;

         // Directional drag multipliers based on rotation. Camera is locked to 45 degrees, so we
         // can use one precalculated value instead of calculating xx, xy, zx and zy individually
         var cameraMultiplier = 0.70710678;

         // Calculate the mouse move amount
         var moveX = currentMouseX - previousMouseX;
         var moveY = currentMouseY - previousMouseY;

         // Adjust axes
         switch (selectedAxisLabel) {
         case AbstractGraph3D.ElementType.AxisXLabel:
             var distance = ((moveX - moveY) * cameraMultiplier) / dragSpeedModifier;
             // Check if we need to change min or max first to avoid invalid ranges
             if (distance > 0) {
                 scatterGraph.axisX.min -= distance;
                 scatterGraph.axisX.max -= distance;
             } else {
                 scatterGraph.axisX.max -= distance;
                 scatterGraph.axisX.min -= distance;
             }
             break;
         case AbstractGraph3D.ElementType.AxisYLabel:
             distance = moveY / dragSpeedModifier;
             // Check if we need to change min or max first to avoid invalid ranges
             if (distance > 0) {
                 scatterGraph.axisY.max += distance;
                 scatterGraph.axisY.min += distance;
             } else {
                 scatterGraph.axisY.min += distance;
                 scatterGraph.axisY.max += distance;
             }
             break;
         case AbstractGraph3D.ElementType.AxisZLabel:
             distance = ((moveX + moveY) * cameraMultiplier) / dragSpeedModifier;
             // Check if we need to change min or max first to avoid invalid ranges
             if (distance > 0) {
                 scatterGraph.axisZ.max += distance;
                 scatterGraph.axisZ.min += distance;
             } else {
                 scatterGraph.axisZ.min += distance;
                 scatterGraph.axisZ.max += distance;
             }
             break;
         }
     }

     Button {
         id: rangeToggle
         // We're adding 3 buttons and want to divide them equally, if not in portrait mode
         width: axisDragView.portraitMode ? parent.width : parent.width / 3
         text: "Use Preset Range"
         anchors.left: parent.left
         anchors.top: parent.top
         property bool autoRange: true
         onClicked: {
             if (autoRange) {
                 text = "Use Automatic Range";
                 scatterGraph.axisX.min = 0.3;
                 scatterGraph.axisX.max = 0.7;
                 scatterGraph.axisY.min = 0.3;
                 scatterGraph.axisY.max = 0.7;
                 scatterGraph.axisZ.min = 0.3;
                 scatterGraph.axisZ.max = 0.7;
                 autoRange = false;
                 dragSpeedModifier = 200.0;
             } else {
                 text = "Use Preset Range";
                 autoRange = true;
                 dragSpeedModifier = 100.0;
             }
             scatterGraph.axisX.autoAdjustRange = autoRange;
             scatterGraph.axisY.autoAdjustRange = autoRange;
             scatterGraph.axisZ.autoAdjustRange = autoRange;
         }
     }

     Button {
         id: orthoToggle
         width: axisDragView.portraitMode ? parent.width : parent.width / 3
         text: "Display Orthographic"
         anchors.left: axisDragView.portraitMode ? parent.left : rangeToggle.right
         anchors.top: axisDragView.portraitMode ? rangeToggle.bottom : parent.top
         onClicked: {
             if (scatterGraph.orthoProjection) {
                 text = "Display Orthographic";
                 scatterGraph.orthoProjection = false;
                 // Orthographic projection disables shadows, so we need to switch them back on
                 scatterGraph.shadowQuality = AbstractGraph3D.ShadowQuality.Medium
             } else {
                 text = "Display Perspective";
                 scatterGraph.orthoProjection = true;
             }
         }
     }

     Button {
         id: exitButton
         width: axisDragView.portraitMode ? parent.width : parent.width / 3
         text: "Quit"
         anchors.left: axisDragView.portraitMode ? parent.left : orthoToggle.right
         anchors.top: axisDragView.portraitMode ? orthoToggle.bottom : parent.top
         onClicked: Qt.quit();
     }
 }