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();
    }
}