From 20257e1e4fbaa6530007d3d5ec8fb35f11df8284 Mon Sep 17 00:00:00 2001 From: Miikka Heikkinen Date: Mon, 4 Nov 2019 16:03:07 +0200 Subject: [PATCH] QmlDesigner: Implement RotateGizmo for 3D edit view Added a gizmo for rotating selected object either freely or locked around X, Y, Z, or camera axis. Change-Id: Ib43c7dd3fc0f49f384d5920fce21ea932c4fc90d Task-number: QDS-1196 Reviewed-by: Mahmoud Badri Reviewed-by: Thomas Hartmann --- .../qml/qmlpuppet/mockfiles/EditView3D.qml | 35 ++- .../qml/qmlpuppet/mockfiles/RotateGizmo.qml | 221 ++++++++++++++++++ .../qml/qmlpuppet/mockfiles/RotateRing.qml | 133 +++++++++++ .../qml/qmlpuppet/mockfiles/meshes/ring.mesh | Bin 0 -> 45528 bytes .../mockfiles/meshes/ringselect.mesh | Bin 0 -> 50008 bytes .../qml2puppet/editor3d/mousearea3d.cpp | 216 +++++++++++++++-- .../qml2puppet/editor3d/mousearea3d.h | 45 +++- share/qtcreator/qml/qmlpuppet/qmlpuppet.qrc | 4 + 8 files changed, 629 insertions(+), 25 deletions(-) create mode 100644 share/qtcreator/qml/qmlpuppet/mockfiles/RotateGizmo.qml create mode 100644 share/qtcreator/qml/qmlpuppet/mockfiles/RotateRing.qml create mode 100644 share/qtcreator/qml/qmlpuppet/mockfiles/meshes/ring.mesh create mode 100644 share/qtcreator/qml/qmlpuppet/mockfiles/meshes/ringselect.mesh diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml index bed8dd3f9a8..2c908755229 100644 --- a/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml @@ -100,12 +100,15 @@ Window { PerspectiveCamera { id: overlayPerspectiveCamera clipFar: editPerspectiveCamera.clipFar + clipNear: editPerspectiveCamera.clipNear position: editPerspectiveCamera.position rotation: editPerspectiveCamera.rotation } OrthographicCamera { id: overlayOrthoCamera + clipFar: editOrthoCamera.clipFar + clipNear: editOrthoCamera.clipNear position: editOrthoCamera.position rotation: editOrthoCamera.rotation } @@ -140,6 +143,21 @@ Window { onScaleChange: viewWindow.changeObjectProperty(selectedNode, "scale") } + RotateGizmo { + id: rotateGizmo + scale: autoScale.getScale(Qt.vector3d(7, 7, 7)) + highlightOnHover: true + targetNode: viewWindow.selectedNode + position: viewWindow.selectedNode ? viewWindow.selectedNode.scenePosition + : Qt.vector3d(0, 0, 0) + globalOrientation: globalControl.checked + visible: selectedNode && btnRotate.selected + view3D: overlayView + + onRotateCommit: viewWindow.commitObjectProperty(selectedNode, "rotation") + onRotateChange: viewWindow.changeObjectProperty(selectedNode, "rotation") + } + AutoScaleHelper { id: autoScale view3D: overlayView @@ -193,12 +211,15 @@ Window { y: 200 z: -300 clipFar: 100000 + clipNear: 1 } OrthographicCamera { id: editOrthoCamera y: 200 z: -300 + clipFar: 100000 + clipNear: 1 } } } @@ -346,7 +367,19 @@ Window { id: usePerspectiveCheckbox checked: true text: qsTr("Use Perspective Projection") - onCheckedChanged: cameraControl.forceActiveFocus() + onCheckedChanged: { + // Since WasdController always acts on active camera, we need to update pos/rot + // to the other camera when we change + if (checked) { + editPerspectiveCamera.position = editOrthoCamera.position; + editPerspectiveCamera.rotation = editOrthoCamera.rotation; + } else { + editOrthoCamera.position = editPerspectiveCamera.position; + editOrthoCamera.rotation = editPerspectiveCamera.rotation; + } + designStudioNativeCameraControlHelper.requestOverlayUpdate(); + cameraControl.forceActiveFocus(); + } } CheckBox { diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/RotateGizmo.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/RotateGizmo.qml new file mode 100644 index 00000000000..b2f42b39e2f --- /dev/null +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/RotateGizmo.qml @@ -0,0 +1,221 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick 2.0 +import QtQuick3D 1.0 +import MouseArea3D 1.0 + +Node { + id: rotateGizmo + + property View3D view3D + property bool highlightOnHover: true + property Node targetNode: null + property bool globalOrientation: true + readonly property bool dragging: cameraRing.dragging + || rotRingX.dragging || rotRingY.dragging || rotRingZ.dragging + property real currentAngle + property point currentMousePos + + signal rotateCommit() + signal rotateChange() + + Rectangle { + id: angleLabel + color: "white" + x: rotateGizmo.currentMousePos.x - (10 + width) + y: rotateGizmo.currentMousePos.y - (10 + height) + width: gizmoLabelText.width + 4 + height: gizmoLabelText.height + 4 + border.width: 1 + visible: rotateGizmo.dragging + parent: rotateGizmo.view3D + + Text { + id: gizmoLabelText + text: { + var l = Qt.locale(); + if (rotateGizmo.targetNode) { + var degrees = currentAngle * (180 / Math.PI); + return qsTr(Number(degrees).toLocaleString(l, 'f', 1)); + } else { + return ""; + } + } + anchors.centerIn: parent + } + } + + Node { + rotation: globalOrientation || !targetNode ? Qt.vector3d(0, 0, 0) : targetNode.sceneRotation + + RotateRing { + id: rotRingX + objectName: "Rotate Ring X" + rotation: Qt.vector3d(0, 90, 0) + targetNode: rotateGizmo.targetNode + color: highlightOnHover && (hovering || dragging) ? Qt.lighter(Qt.rgba(1, 0, 0, 1)) + : Qt.rgba(1, 0, 0, 1) + priority: 40 + view3D: rotateGizmo.view3D + active: rotateGizmo.visible + + onRotateCommit: rotateGizmo.rotateCommit() + onRotateChange: rotateGizmo.rotateChange() + onCurrentAngleChanged: rotateGizmo.currentAngle = currentAngle + onCurrentMousePosChanged: rotateGizmo.currentMousePos = currentMousePos + } + + RotateRing { + id: rotRingY + objectName: "Rotate Ring Y" + rotation: Qt.vector3d(90, 0, 0) + targetNode: rotateGizmo.targetNode + color: highlightOnHover && (hovering || dragging) ? Qt.lighter(Qt.rgba(0, 0.6, 0, 1)) + : Qt.rgba(0, 0.6, 0, 1) + // Just a smidge smaller than higher priority rings so that it doesn't obscure them + scale: Qt.vector3d(0.998, 0.998, 0.998) + priority: 30 + view3D: rotateGizmo.view3D + active: rotateGizmo.visible + + onRotateCommit: rotateGizmo.rotateCommit() + onRotateChange: rotateGizmo.rotateChange() + onCurrentAngleChanged: rotateGizmo.currentAngle = currentAngle + onCurrentMousePosChanged: rotateGizmo.currentMousePos = currentMousePos + } + + RotateRing { + id: rotRingZ + objectName: "Rotate Ring Z" + rotation: Qt.vector3d(0, 0, 0) + targetNode: rotateGizmo.targetNode + color: highlightOnHover && (hovering || dragging) ? Qt.lighter(Qt.rgba(0, 0, 1, 1)) + : Qt.rgba(0, 0, 1, 1) + // Just a smidge smaller than higher priority rings so that it doesn't obscure them + scale: Qt.vector3d(0.996, 0.996, 0.996) + priority: 20 + view3D: rotateGizmo.view3D + active: rotateGizmo.visible + + onRotateCommit: rotateGizmo.rotateCommit() + onRotateChange: rotateGizmo.rotateChange() + onCurrentAngleChanged: rotateGizmo.currentAngle = currentAngle + onCurrentMousePosChanged: rotateGizmo.currentMousePos = currentMousePos + } + } + + RotateRing { + id: cameraRing + objectName: "cameraRing" + rotation: rotateGizmo.view3D.camera.rotation + targetNode: rotateGizmo.targetNode + color: highlightOnHover && (hovering || dragging) ? Qt.lighter(Qt.rgba(0.5, 0.5, 0.5, 1)) + : Qt.rgba(0.5, 0.5, 0.5, 1) + // Just a smidge smaller than higher priority rings so that it doesn't obscure them + scale: Qt.vector3d(0.994, 0.994, 0.994) + priority: 10 + view3D: rotateGizmo.view3D + active: rotateGizmo.visible + + onRotateCommit: rotateGizmo.rotateCommit() + onRotateChange: rotateGizmo.rotateChange() + onCurrentAngleChanged: rotateGizmo.currentAngle = currentAngle + onCurrentMousePosChanged: rotateGizmo.currentMousePos = currentMousePos + } + + Model { + id: freeRotator + + source: "#Sphere" + materials: DefaultMaterial { + id: material + emissiveColor: "black" + opacity: mouseAreaFree.hovering ? 0.15 : 0 + lighting: DefaultMaterial.NoLighting + } + scale: Qt.vector3d(0.15, 0.15, 0.15) + + property vector3d _pointerPosPressed + property vector3d _targetPosOnScreen + property vector3d _startRotation + + function handlePressed(screenPos) + { + if (!rotateGizmo.targetNode) + return; + + _targetPosOnScreen = view3D.mapFrom3DScene(rotateGizmo.targetNode.scenePosition); + _targetPosOnScreen.z = 0; + _pointerPosPressed = Qt.vector3d(screenPos.x, screenPos.y, 0); + + // Recreate vector so we don't follow the changes in targetNode.rotation + _startRotation = Qt.vector3d(rotateGizmo.targetNode.rotation.x, + rotateGizmo.targetNode.rotation.y, + rotateGizmo.targetNode.rotation.z); + } + + function handleDragged(screenPos) + { + if (!rotateGizmo.targetNode) + return; + + mouseAreaFree.applyFreeRotation( + rotateGizmo.targetNode, _startRotation, _pointerPosPressed, + Qt.vector3d(screenPos.x, screenPos.y, 0), _targetPosOnScreen); + + rotateGizmo.rotateChange(); + } + + function handleReleased(screenPos) + { + if (!rotateGizmo.targetNode) + return; + + mouseAreaFree.applyFreeRotation( + rotateGizmo.targetNode, _startRotation, _pointerPosPressed, + Qt.vector3d(screenPos.x, screenPos.y, 0), _targetPosOnScreen); + + rotateGizmo.rotateCommit(); + } + + MouseArea3D { + id: mouseAreaFree + view3D: rotateGizmo.view3D + rotation: rotateGizmo.view3D.camera.rotation + objectName: "Free rotator plane" + x: -50 + y: -50 + width: 100 + height: 100 + circlePickArea: Qt.point(25, 50) + grabsMouse: rotateGizmo.targetNode + active: rotateGizmo.visible + onPressed: freeRotator.handlePressed(screenPos) + onDragged: freeRotator.handleDragged(screenPos) + onReleased: freeRotator.handleReleased(screenPos) + } + } +} diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/RotateRing.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/RotateRing.qml new file mode 100644 index 00000000000..634eb017c9b --- /dev/null +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/RotateRing.qml @@ -0,0 +1,133 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick 2.0 +import QtQuick3D 1.0 +import MouseArea3D 1.0 + +Model { + id: rotateRing + + property View3D view3D + property alias color: material.emissiveColor + property Node targetNode: null + property bool dragging: false + property bool active: false + property alias hovering: mouseAreaMain.hovering + property alias priority: mouseAreaMain.priority + property real currentAngle + property point currentMousePos + + property vector3d _pointerPosPressed + property vector3d _targetPosOnScreen + property vector3d _startRotation + property bool _trackBall + + signal rotateCommit() + signal rotateChange() + + source: "meshes/ring.mesh" + + Model { + id: pickModel + objectName: "PickModel for " + rotateRing.objectName + source: "meshes/ringselect.mesh" + pickable: true + } + + materials: DefaultMaterial { + id: material + emissiveColor: "white" + lighting: DefaultMaterial.NoLighting + } + + function applyLocalRotation(screenPos) + { + currentAngle = mouseAreaMain.getNewRotationAngle(targetNode, _pointerPosPressed, + Qt.vector3d(screenPos.x, screenPos.y, 0), + _targetPosOnScreen, currentAngle, + _trackBall); + mouseAreaMain.applyRotationAngleToNode(targetNode, _startRotation, currentAngle); + } + + function handlePressed(screenPos, angle) + { + if (!targetNode) + return; + + _targetPosOnScreen = view3D.mapFrom3DScene(targetNode.scenePosition); + _targetPosOnScreen.z = 0; + _pointerPosPressed = Qt.vector3d(screenPos.x, screenPos.y, 0); + dragging = true; + _trackBall = angle < 0.1; + + // Recreate vector so we don't follow the changes in targetNode.rotation + _startRotation = Qt.vector3d(targetNode.rotation.x, + targetNode.rotation.y, + targetNode.rotation.z); + currentAngle = 0; + currentMousePos = screenPos; + } + + function handleDragged(screenPos) + { + if (!targetNode) + return; + + applyLocalRotation(screenPos); + currentMousePos = screenPos; + rotateChange(); + } + + function handleReleased(screenPos) + { + if (!targetNode) + return; + + applyLocalRotation(screenPos); + rotateCommit(); + dragging = false; + currentAngle = 0; + currentMousePos = screenPos; + } + + MouseArea3D { + id: mouseAreaMain + view3D: rotateRing.view3D + objectName: "Main plane of " + rotateRing.objectName + x: -30 + y: -30 + width: 60 + height: 60 + circlePickArea: Qt.point(9.2, 1.4) + grabsMouse: targetNode + active: rotateRing.active + pickNode: pickModel + minAngle: 0.05 + onPressed: rotateRing.handlePressed(screenPos, angle) + onDragged: rotateRing.handleDragged(screenPos) + onReleased: rotateRing.handleReleased(screenPos) + } +} diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/meshes/ring.mesh b/share/qtcreator/qml/qmlpuppet/mockfiles/meshes/ring.mesh new file mode 100644 index 0000000000000000000000000000000000000000..56e1b82f29db01f0d26af02fa513a194a118d38a GIT binary patch literal 45528 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 literal 0 HcmV?d00001 diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/meshes/ringselect.mesh b/share/qtcreator/qml/qmlpuppet/mockfiles/meshes/ringselect.mesh new file mode 100644 index 0000000000000000000000000000000000000000..b110b308f03d66c44c9d456ab8eaa601c90df3ae GIT binary patch literal 50008 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 J6k%)-4FL4P-);Z^ literal 0 HcmV?d00001 diff --git a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.cpp b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.cpp index d4135de9732..31ed6611250 100644 --- a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.cpp +++ b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.cpp @@ -29,11 +29,15 @@ #include #include +#include +#include +#include namespace QmlDesigner { namespace Internal { MouseArea3D *MouseArea3D::s_mouseGrab = nullptr; +static const qreal s_mouseDragMultiplier = .02; MouseArea3D::MouseArea3D(QQuick3DNode *parent) : QQuick3DNode(parent) @@ -65,6 +69,21 @@ bool MouseArea3D::active() const return m_active; } +QPointF MouseArea3D::circlePickArea() const +{ + return m_circlePickArea; +} + +qreal MouseArea3D::minAngle() const +{ + return m_minAngle; +} + +QQuick3DNode *MouseArea3D::pickNode() const +{ + return m_pickNode; +} + qreal MouseArea3D::x() const { return m_x; @@ -105,7 +124,7 @@ void MouseArea3D::setGrabsMouse(bool grabsMouse) return; m_grabsMouse = grabsMouse; - emit grabsMouseChanged(grabsMouse); + emit grabsMouseChanged(); } void MouseArea3D::setActive(bool active) @@ -114,7 +133,37 @@ void MouseArea3D::setActive(bool active) return; m_active = active; - emit activeChanged(active); + emit activeChanged(); +} + +void MouseArea3D::setCirclePickArea(const QPointF &pickArea) +{ + if (m_circlePickArea == pickArea) + return; + + m_circlePickArea = pickArea; + emit circlePickAreaChanged(); +} + +// This is the minimum angle for circle picking. At lower angles we fall back to picking on pickNode +void MouseArea3D::setMinAngle(qreal angle) +{ + if (qFuzzyCompare(m_minAngle, angle)) + return; + + m_minAngle = angle; + emit minAngleChanged(); +} + +// This is the fallback pick node when circle picking can't be done due to low angle +// Pick node can't be used except in low angles, as long as only bounding box picking is supported +void MouseArea3D::setPickNode(QQuick3DNode *node) +{ + if (m_pickNode == node) + return; + + m_pickNode = node; + emit pickNodeChanged(); } void MouseArea3D::setX(qreal x) @@ -123,7 +172,7 @@ void MouseArea3D::setX(qreal x) return; m_x = x; - emit xChanged(x); + emit xChanged(); } void MouseArea3D::setY(qreal y) @@ -132,7 +181,7 @@ void MouseArea3D::setY(qreal y) return; m_y = y; - emit yChanged(y); + emit yChanged(); } void MouseArea3D::setWidth(qreal width) @@ -141,7 +190,7 @@ void MouseArea3D::setWidth(qreal width) return; m_width = width; - emit widthChanged(width); + emit widthChanged(); } void MouseArea3D::setHeight(qreal height) @@ -150,7 +199,7 @@ void MouseArea3D::setHeight(qreal height) return; m_height = height; - emit heightChanged(height); + emit heightChanged(); } void MouseArea3D::setPriority(int level) @@ -159,7 +208,7 @@ void MouseArea3D::setPriority(int level) return; m_priority = level; - emit priorityChanged(level); + emit priorityChanged(); } void MouseArea3D::componentComplete() @@ -278,6 +327,84 @@ QVector3D MouseArea3D::getNewScale(QQuick3DNode *node, const QVector3D &startSca return startScale; } +qreal QmlDesigner::Internal::MouseArea3D::getNewRotationAngle( + QQuick3DNode *node, const QVector3D &pressPos, const QVector3D ¤tPos, + const QVector3D &nodePos, qreal prevAngle, bool trackBall) +{ + const QVector3D cameraToNodeDir = getCameraToNodeDir(node); + if (trackBall) { + // Only the distance in plane direction is relevant in trackball drag + QVector3D dragDir = QVector3D::crossProduct(getNormal(), cameraToNodeDir).normalized(); + QVector3D screenDragDir = m_view3D->mapFrom3DScene(node->scenePosition() + dragDir); + screenDragDir.setZ(0); + dragDir = (screenDragDir - nodePos).normalized(); + const QVector3D pressToCurrent = (currentPos - pressPos); + float magnitude = QVector3D::dotProduct(pressToCurrent, dragDir); + qreal angle = -s_mouseDragMultiplier * qreal(magnitude); + return angle; + } else { + const QVector3D nodeToPress = (pressPos - nodePos).normalized(); + const QVector3D nodeToCurrent = (currentPos - nodePos).normalized(); + qreal angle = qAcos(qreal(QVector3D::dotProduct(nodeToPress, nodeToCurrent))); + + // Determine drag direction left/right + const QVector3D dragNormal = QVector3D::crossProduct(nodeToPress, nodeToCurrent).normalized(); + angle *= QVector3D::dotProduct(QVector3D(0.f, 0.f, 1.f), dragNormal) < 0 ? -1.0 : 1.0; + + // Determine drag ring orientation relative to camera + angle *= QVector3D::dotProduct(getNormal(), cameraToNodeDir) < 0 ? 1.0 : -1.0; + + qreal adjustedPrevAngle = prevAngle; + const qreal PI_2 = M_PI * 2.0; + while (adjustedPrevAngle < -PI_2) + adjustedPrevAngle += PI_2; + while (adjustedPrevAngle > PI_2) + adjustedPrevAngle -= PI_2; + + // at M_PI rotation, the angle flips to negative + if (qAbs(angle - adjustedPrevAngle) > M_PI) { + if (angle > adjustedPrevAngle) + return prevAngle - (PI_2 - angle + adjustedPrevAngle); + else + return prevAngle + (PI_2 + angle - adjustedPrevAngle); + } else { + return prevAngle + angle - adjustedPrevAngle; + } + } + +} + +void QmlDesigner::Internal::MouseArea3D::applyRotationAngleToNode( + QQuick3DNode *node, const QVector3D &startRotation, qreal angle) +{ + if (!qFuzzyIsNull(angle)) { + node->setRotation(startRotation); + node->rotate(qRadiansToDegrees(angle), getNormal(), QQuick3DNode::SceneSpace); + } +} + +void MouseArea3D::applyFreeRotation(QQuick3DNode *node, const QVector3D &startRotation, + const QVector3D &pressPos, const QVector3D ¤tPos) +{ + QVector3D dragVector = currentPos - pressPos; + + if (dragVector.length() < 0.001f) + return; + + const float *dataPtr(sceneTransform().data()); + QVector3D xAxis = QVector3D(-dataPtr[0], -dataPtr[1], -dataPtr[2]).normalized(); + QVector3D yAxis = QVector3D(-dataPtr[4], -dataPtr[5], -dataPtr[6]).normalized(); + + QVector3D finalAxis = (dragVector.x() * yAxis + dragVector.y() * xAxis); + + qreal degrees = qRadiansToDegrees(qreal(finalAxis.length()) * s_mouseDragMultiplier); + + finalAxis.normalize(); + + node->setRotation(startRotation); + node->rotate(degrees, finalAxis, QQuick3DNode::SceneSpace); +} + QVector3D MouseArea3D::getMousePosInPlane(const QPointF &mousePosInView) const { const QVector3D mousePos1(float(mousePosInView.x()), float(mousePosInView.y()), 0); @@ -300,12 +427,50 @@ bool MouseArea3D::eventFilter(QObject *, QEvent *event) return false; } - auto mouseOnTopOfMouseArea = [this](const QVector3D &mousePosInPlane) -> bool { - return !qFuzzyCompare(mousePosInPlane.z(), -1) + qreal pickAngle = 0.; + + auto mouseOnTopOfMouseArea = [this, &pickAngle]( + const QVector3D &mousePosInPlane, const QPointF &mousePos) -> bool { + const bool onPlane = !qFuzzyCompare(mousePosInPlane.z(), -1) && mousePosInPlane.x() >= float(m_x) && mousePosInPlane.x() <= float(m_x + m_width) && mousePosInPlane.y() >= float(m_y) && mousePosInPlane.y() <= float(m_y + m_height); + + bool onCircle = true; + bool pickSuccess = false; + if (!qFuzzyIsNull(m_circlePickArea.y()) || !qFuzzyIsNull(m_minAngle)) { + + QVector3D cameraToMouseAreaDir = getCameraToNodeDir(this); + const QVector3D mouseAreaDir = getNormal(); + qreal angle = qreal(QVector3D::dotProduct(cameraToMouseAreaDir, mouseAreaDir)); + // Do not allow selecting ring that is nearly perpendicular to camera, as dragging along + // that plane would be difficult + pickAngle = qAcos(angle); + pickAngle = pickAngle > M_PI_2 ? pickAngle - M_PI_2 : M_PI_2 - pickAngle; + if (pickAngle > m_minAngle) { + if (!qFuzzyIsNull(m_circlePickArea.y())) { + qreal ringCenter = m_circlePickArea.x(); + // Thickness is increased according to the angle to camera to keep projected + // circle thickness constant at all angles. + qreal divisor = qSin(pickAngle) * 2.; // This is never zero + qreal thickness = ((m_circlePickArea.y() / divisor)); + qreal mousePosRadius = qSqrt(qreal(mousePosInPlane.x() * mousePosInPlane.x()) + + qreal(mousePosInPlane.y() * mousePosInPlane.y())); + onCircle = ringCenter - thickness <= mousePosRadius + && ringCenter + thickness >= mousePosRadius; + } + } else { + // Fall back to picking on the pickNode. At this angle, bounding box pick is not + // a problem + onCircle = false; + if (m_pickNode) { + QQuick3DPickResult pr = m_view3D->pick(float(mousePos.x()), float(mousePos.y())); + pickSuccess = pr.objectHit() == m_pickNode; + } + } + } + return (onCircle && onPlane) || pickSuccess; }; switch (event->type()) { @@ -313,9 +478,9 @@ bool MouseArea3D::eventFilter(QObject *, QEvent *event) auto const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::LeftButton) { m_mousePosInPlane = getMousePosInPlane(mouseEvent->pos()); - if (mouseOnTopOfMouseArea(m_mousePosInPlane)) { + if (mouseOnTopOfMouseArea(m_mousePosInPlane, mouseEvent->pos())) { setDragging(true); - emit pressed(m_mousePosInPlane, mouseEvent->globalPos()); + emit pressed(m_mousePosInPlane, mouseEvent->pos(), pickAngle); if (m_grabsMouse) { if (s_mouseGrab && s_mouseGrab != this) { s_mouseGrab->setDragging(false); @@ -338,13 +503,13 @@ bool MouseArea3D::eventFilter(QObject *, QEvent *event) if (qFuzzyCompare(mousePosInPlane.z(), -1)) mousePosInPlane = m_mousePosInPlane; setDragging(false); - emit released(mousePosInPlane, mouseEvent->globalPos()); + emit released(mousePosInPlane, mouseEvent->pos()); if (m_grabsMouse) { if (s_mouseGrab && s_mouseGrab != this) { s_mouseGrab->setDragging(false); s_mouseGrab->setHovering(false); } - if (mouseOnTopOfMouseArea(mousePosInPlane)) { + if (mouseOnTopOfMouseArea(mousePosInPlane, mouseEvent->pos())) { s_mouseGrab = this; setHovering(true); } else { @@ -362,7 +527,7 @@ bool MouseArea3D::eventFilter(QObject *, QEvent *event) case QEvent::HoverMove: { auto const mouseEvent = static_cast(event); const QVector3D mousePosInPlane = getMousePosInPlane(mouseEvent->pos()); - const bool hasMouse = mouseOnTopOfMouseArea(mousePosInPlane); + const bool hasMouse = mouseOnTopOfMouseArea(mousePosInPlane, mouseEvent->pos()); setHovering(hasMouse); @@ -376,9 +541,9 @@ bool MouseArea3D::eventFilter(QObject *, QEvent *event) s_mouseGrab = nullptr; } - if (m_dragging && !qFuzzyCompare(mousePosInPlane.z(), -1)) { + if (m_dragging && (m_circlePickArea.y() > 0. || !qFuzzyCompare(mousePosInPlane.z(), -1))) { m_mousePosInPlane = mousePosInPlane; - emit dragged(mousePosInPlane, mouseEvent->globalPos()); + emit dragged(mousePosInPlane, mouseEvent->pos()); } break; @@ -408,6 +573,25 @@ void MouseArea3D::setHovering(bool enable) emit hoveringChanged(); } +QVector3D MouseArea3D::getNormal() const +{ + const float *dataPtr(sceneTransform().data()); + return QVector3D(dataPtr[8], dataPtr[9], dataPtr[10]).normalized(); +} + +QVector3D MouseArea3D::getCameraToNodeDir(QQuick3DNode *node) const +{ + QVector3D dir; + if (qobject_cast(m_view3D->camera())) { + dir = m_view3D->camera()->cameraNode()->getDirection(); + // Camera direction has x and y flipped + dir = QVector3D(-dir.x(), -dir.y(), dir.z()); + } else { + dir = (node->scenePosition() - m_view3D->camera()->scenePosition()).normalized(); + } + return dir; +} + } } diff --git a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.h b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.h index af465576829..e227b3f9dde 100644 --- a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.h +++ b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/mousearea3d.h @@ -28,9 +28,11 @@ #ifdef QUICK3D_MODULE #include +#include #include #include +#include #include #include @@ -50,6 +52,9 @@ class MouseArea3D : public QQuick3DNode Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) Q_PROPERTY(int priority READ priority WRITE setPriority NOTIFY priorityChanged) Q_PROPERTY(int active READ active WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(QPointF circlePickArea READ circlePickArea WRITE setCirclePickArea NOTIFY circlePickAreaChanged) + Q_PROPERTY(qreal minAngle READ minAngle WRITE setMinAngle NOTIFY minAngleChanged) + Q_PROPERTY(QQuick3DNode *pickNode READ pickNode WRITE setPickNode NOTIFY pickNodeChanged) Q_INTERFACES(QQmlParserStatus) @@ -68,11 +73,17 @@ public: bool dragging() const; bool grabsMouse() const; bool active() const; + QPointF circlePickArea() const; + qreal minAngle() const; + QQuick3DNode *pickNode() const; public slots: void setView3D(QQuick3DViewport *view3D); void setGrabsMouse(bool grabsMouse); void setActive(bool active); + void setCirclePickArea(const QPointF &pickArea); + void setMinAngle(qreal angle); + void setPickNode(QQuick3DNode *node); void setX(qreal x); void setY(qreal y); @@ -89,22 +100,35 @@ public slots: const QVector3D &pressPos, const QVector3D &sceneRelativeDistance, bool global); + Q_INVOKABLE qreal getNewRotationAngle(QQuick3DNode *node, const QVector3D &pressPos, + const QVector3D ¤tPos, const QVector3D &nodePos, + qreal prevAngle, bool trackBall); + Q_INVOKABLE void applyRotationAngleToNode(QQuick3DNode *node, const QVector3D &startRotation, + qreal angle); + Q_INVOKABLE void applyFreeRotation(QQuick3DNode *node, const QVector3D &startRotation, + const QVector3D &pressPos, const QVector3D ¤tPos); + signals: void view3DChanged(); - void xChanged(qreal x); - void yChanged(qreal y); - void widthChanged(qreal width); - void heightChanged(qreal height); - void priorityChanged(int level); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void priorityChanged(); void hoveringChanged(); void draggingChanged(); - void activeChanged(bool active); - void pressed(const QVector3D &scenePos, const QPoint &screenPos); + void activeChanged(); + void grabsMouseChanged(); + void circlePickAreaChanged(); + void minAngleChanged(); + void pickNodeChanged(); + + // angle parameter is only set if circlePickArea is specified + void pressed(const QVector3D &scenePos, const QPoint &screenPos, qreal angle); void released(const QVector3D &scenePos, const QPoint &screenPos); void dragged(const QVector3D &scenePos, const QPoint &screenPos); - void grabsMouseChanged(bool grabsMouse); protected: void classBegin() override {} @@ -114,6 +138,8 @@ protected: private: void setDragging(bool enable); void setHovering(bool enable); + QVector3D getNormal() const; + QVector3D getCameraToNodeDir(QQuick3DNode *node) const; Q_DISABLE_COPY(MouseArea3D) QQuick3DViewport *m_view3D = nullptr; @@ -133,6 +159,9 @@ private: static MouseArea3D *s_mouseGrab; bool m_grabsMouse; QVector3D m_mousePosInPlane; + QPointF m_circlePickArea; + qreal m_minAngle = 0.; + QQuick3DNode *m_pickNode = nullptr; }; } diff --git a/share/qtcreator/qml/qmlpuppet/qmlpuppet.qrc b/share/qtcreator/qml/qmlpuppet/qmlpuppet.qrc index fbdec743e41..deb70ba1453 100644 --- a/share/qtcreator/qml/qmlpuppet/qmlpuppet.qrc +++ b/share/qtcreator/qml/qmlpuppet/qmlpuppet.qrc @@ -22,8 +22,12 @@ mockfiles/ScaleRod.qml mockfiles/ScaleGizmo.qml mockfiles/ToolBarButton.qml + mockfiles/RotateGizmo.qml + mockfiles/RotateRing.qml mockfiles/meshes/arrow.mesh mockfiles/meshes/scalerod.mesh + mockfiles/meshes/ring.mesh + mockfiles/meshes/ringselect.mesh mockfiles/images/camera-pick-icon.png mockfiles/images/camera-pick-icon@2x.png mockfiles/images/light-pick-icon.png