QmlDesigner: Implement better camera navigation in 3D edit view

Edit camera is now controlled as in Qt 3D Studio:
ALT + left button orbits camera.
ALT + middle button pans camera.
ALT + right button zooms camera.
Wheel zooms camera.

Task-number: QDS-1206
Change-Id: Ia72644073d172b00483ceed8bcc5ffb8dce68741
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Miikka Heikkinen
2019-11-15 15:20:31 +02:00
parent 668cd2091a
commit 77cb514a79
14 changed files with 303 additions and 145 deletions

View File

@@ -46,7 +46,7 @@ Node {
}
Connections {
target: designStudioNativeCameraControlHelper
target: _generalHelper
onOverlayUpdateNeeded: updateScale()
}

View File

@@ -0,0 +1,118 @@
/****************************************************************************
**
** 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.12
import QtQuick3D 1.0
Item {
id: cameraCtrl
property Camera camera: null
property vector3d _lookAtPoint
property vector3d _pressPoint
property vector3d _prevPoint
property vector3d _startRotation
property vector3d _startPosition
property vector3d _startLookAtPoint
property matrix4x4 _startTransform
property bool _dragging
property int _button
property real _zoomFactor: 1
property real _defaultCameraLookAtDistance: 0
property Camera _prevCamera: null
function zoomRelative(distance)
{
_zoomFactor = _generalHelper.zoomCamera(camera, distance, _defaultCameraLookAtDistance,
_lookAtPoint, _zoomFactor, true);
}
Component.onCompleted: {
cameraCtrl._defaultCameraLookAtDistance = cameraCtrl.camera.position.length();
}
onCameraChanged: {
if (_prevCamera) {
// Reset zoom on previous camera to ensure it's properties are good to copy to new cam
_generalHelper.zoomCamera(_prevCamera, 0, _defaultCameraLookAtDistance, _lookAtPoint,
1, false);
camera.position = _prevCamera.position;
camera.rotation = _prevCamera.rotation;
// Apply correct zoom to new camera
_generalHelper.zoomCamera(camera, 0, _defaultCameraLookAtDistance, _lookAtPoint,
_zoomFactor, false);
}
_prevCamera = camera;
}
MouseArea {
id: mouseHandler
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: false
anchors.fill: parent
onPositionChanged: {
if (mouse.modifiers === Qt.AltModifier && cameraCtrl._dragging) {
var currentPoint = Qt.vector3d(mouse.x, mouse.y, 0);
if (cameraCtrl._button == Qt.LeftButton) {
_generalHelper.orbitCamera(cameraCtrl.camera, cameraCtrl._startRotation,
cameraCtrl._lookAtPoint, cameraCtrl._pressPoint,
currentPoint);
} else if (cameraCtrl._button == Qt.MiddleButton) {
cameraCtrl._lookAtPoint = _generalHelper.panCamera(
cameraCtrl.camera, cameraCtrl._startTransform,
cameraCtrl._startPosition, cameraCtrl._startLookAtPoint,
cameraCtrl._pressPoint, currentPoint, _zoomFactor);
} else if (cameraCtrl._button == Qt.RightButton) {
cameraCtrl.zoomRelative(currentPoint.y - cameraCtrl._prevPoint.y)
cameraCtrl._prevPoint = currentPoint;
}
}
}
onPressed: {
if (mouse.modifiers === Qt.AltModifier) {
cameraCtrl._dragging = true;
cameraCtrl._startRotation = cameraCtrl.camera.rotation;
cameraCtrl._startPosition = cameraCtrl.camera.position;
cameraCtrl._startLookAtPoint = _lookAtPoint;
cameraCtrl._pressPoint = Qt.vector3d(mouse.x, mouse.y, 0);
cameraCtrl._prevPoint = cameraCtrl._pressPoint;
cameraCtrl._button = mouse.button;
cameraCtrl._startTransform = cameraCtrl.camera.sceneTransform;
} else {
mouse.accepted = false;
}
}
onReleased: cameraCtrl._dragging = false;
onCanceled: cameraCtrl._dragging = false;
onWheel: {
// Emprically determined divisor for nice zoom
cameraCtrl.zoomRelative(wheel.angleDelta.y / -40);
}
}
}

View File

@@ -26,7 +26,6 @@
import QtQuick 2.12
import QtQuick.Window 2.0
import QtQuick3D 1.0
import QtQuick3D.Helpers 1.0
import QtQuick.Controls 2.0
import QtGraphicalEffects 1.0
@@ -78,7 +77,7 @@ Window {
{
var component = Qt.createComponent("CameraGizmo.qml");
if (component.status === Component.Ready) {
var geometryName = designStudioNativeCameraControlHelper.generateUniqueName("CameraGeometry");
var geometryName = _generalHelper.generateUniqueName("CameraGeometry");
var gizmo = component.createObject(
overlayScene,
{"view3D": overlayView, "targetNode": obj, "geometryName": geometryName,
@@ -92,10 +91,10 @@ Window {
// Work-around the fact that the projection matrix for the camera is not calculated until
// the first frame is rendered, so any initial calls to mapFrom3DScene() will fail.
Component.onCompleted: designStudioNativeCameraControlHelper.requestOverlayUpdate();
Component.onCompleted: _generalHelper.requestOverlayUpdate();
onWidthChanged: designStudioNativeCameraControlHelper.requestOverlayUpdate();
onHeightChanged: designStudioNativeCameraControlHelper.requestOverlayUpdate();
onWidthChanged: _generalHelper.requestOverlayUpdate();
onHeightChanged: _generalHelper.requestOverlayUpdate();
Node {
id: overlayScene
@@ -114,6 +113,7 @@ Window {
clipNear: editOrthoCamera.clipNear
position: editOrthoCamera.position
rotation: editOrthoCamera.rotation
scale: editOrthoCamera.scale
}
MoveGizmo {
@@ -209,11 +209,14 @@ Window {
linearFade: 0
}
// Initial camera position and rotation should be such that they look at origin.
// Otherwise EditCameraController._lookAtPoint needs to be initialized to correct
// point.
PerspectiveCamera {
id: editPerspectiveCamera
z: -600
y: 200
rotation.x: 30
y: 600
rotation.x: 45
clipFar: 100000
clipNear: 1
}
@@ -221,8 +224,8 @@ Window {
OrthographicCamera {
id: editOrthoCamera
z: -600
y: 200
rotation.x: 30
y: 600
rotation.x: 45
clipFar: 100000
clipNear: 1
}
@@ -273,19 +276,10 @@ Window {
}
}
WasdController {
EditCameraController {
id: cameraControl
controlledObject: editView.camera
acceptedButtons: Qt.RightButton
onInputsNeedProcessingChanged: designStudioNativeCameraControlHelper.enabled
= cameraControl.inputsNeedProcessing
// Use separate native timer as QML timers don't work inside Qt Design Studio
Connections {
target: designStudioNativeCameraControlHelper
onUpdateInputs: cameraControl.processInputs()
}
camera: editView.camera
anchors.fill: parent
}
}
@@ -365,39 +359,25 @@ Window {
id: editLightCheckbox
checked: false
text: qsTr("Use Edit View Light")
onCheckedChanged: cameraControl.forceActiveFocus()
}
CheckBox {
id: usePerspectiveCheckbox
checked: true
text: qsTr("Use Perspective Projection")
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();
}
onCheckedChanged: _generalHelper.requestOverlayUpdate()
}
CheckBox {
id: globalControl
checked: true
text: qsTr("Use Global Orientation")
onCheckedChanged: cameraControl.forceActiveFocus()
}
}
Text {
id: helpText
text: qsTr("Camera: W,A,S,D,R,F,right mouse drag")
text: qsTr("Camera controls: ALT + mouse press and drag. Left: Rotate, Middle: Pan, Right/Wheel: Zoom.")
anchors.bottom: parent.bottom
}
}

View File

@@ -49,7 +49,7 @@ Item {
}
Connections {
target: designStudioNativeCameraControlHelper
target: _generalHelper
onOverlayUpdateNeeded: updateOverlay()
}

View File

@@ -1,78 +0,0 @@
/****************************************************************************
**
** 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.
**
****************************************************************************/
#include "cameracontrolhelper.h"
#include <QHash>
namespace QmlDesigner {
namespace Internal {
CameraControlHelper::CameraControlHelper()
: QObject()
{
m_inputUpdateTimer.setInterval(16);
QObject::connect(&m_inputUpdateTimer, &QTimer::timeout,
this, &CameraControlHelper::handleUpdateTimer);
m_overlayUpdateTimer.setInterval(16);
m_overlayUpdateTimer.setSingleShot(true);
QObject::connect(&m_overlayUpdateTimer, &QTimer::timeout,
this, &CameraControlHelper::overlayUpdateNeeded);
}
bool CameraControlHelper::enabled()
{
return m_enabled;
}
void CameraControlHelper::handleUpdateTimer()
{
emit updateInputs();
}
void CameraControlHelper::setEnabled(bool enabled)
{
if (enabled)
m_inputUpdateTimer.start();
else
m_inputUpdateTimer.stop();
m_enabled = enabled;
}
void CameraControlHelper::requestOverlayUpdate()
{
if (!m_overlayUpdateTimer.isActive())
m_overlayUpdateTimer.start();
}
QString CameraControlHelper::generateUniqueName(const QString &nameRoot)
{
static QHash<QString, int> counters;
int count = counters[nameRoot]++;
return QStringLiteral("%1_%2").arg(nameRoot).arg(count);
}
}
}

View File

@@ -1,9 +1,9 @@
HEADERS += $$PWD/cameracontrolhelper.h \
HEADERS += $$PWD/generalhelper.h \
$$PWD/mousearea3d.h \
$$PWD/camerageometry.h \
$$PWD/gridgeometry.h
SOURCES += $$PWD/cameracontrolhelper.cpp \
SOURCES += $$PWD/generalhelper.cpp \
$$PWD/mousearea3d.cpp \
$$PWD/camerageometry.cpp \
$$PWD/gridgeometry.cpp

View File

@@ -0,0 +1,132 @@
/****************************************************************************
**
** 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.
**
****************************************************************************/
#include "generalhelper.h"
#ifdef QUICK3D_MODULE
#include <QtQuick3D/private/qquick3dorthographiccamera_p.h>
#include <QtQuick3D/private/qquick3dperspectivecamera_p.h>
#include <QtCore/qhash.h>
#include <QtGui/qmatrix4x4.h>
namespace QmlDesigner {
namespace Internal {
GeneralHelper::GeneralHelper()
: QObject()
{
m_overlayUpdateTimer.setInterval(16);
m_overlayUpdateTimer.setSingleShot(true);
QObject::connect(&m_overlayUpdateTimer, &QTimer::timeout,
this, &GeneralHelper::overlayUpdateNeeded);
}
void GeneralHelper::requestOverlayUpdate()
{
if (!m_overlayUpdateTimer.isActive())
m_overlayUpdateTimer.start();
}
QString GeneralHelper::generateUniqueName(const QString &nameRoot)
{
static QHash<QString, int> counters;
int count = counters[nameRoot]++;
return QStringLiteral("%1_%2").arg(nameRoot).arg(count);
}
void GeneralHelper::orbitCamera(QQuick3DCamera *camera, const QVector3D &startRotation,
const QVector3D &lookAtPoint, const QVector3D &pressPos,
const QVector3D &currentPos)
{
QVector3D dragVector = currentPos - pressPos;
if (dragVector.length() < 0.001f)
return;
camera->setRotation(startRotation);
QVector3D newRotation(dragVector.y(), dragVector.x(), 0.f);
newRotation *= 0.5f; // Emprically determined multiplier for nice drag
newRotation += startRotation;
camera->setRotation(newRotation);
const QVector3D oldLookVector = camera->position() - lookAtPoint;
QMatrix4x4 m = camera->sceneTransform();
const float *dataPtr(m.data());
QVector3D newLookVector(-dataPtr[8], -dataPtr[9], -dataPtr[10]);
newLookVector.normalize();
newLookVector *= oldLookVector.length();
camera->setPosition(lookAtPoint + newLookVector);
}
// Pans camera and returns the new look-at point
QVector3D GeneralHelper::panCamera(QQuick3DCamera *camera, const QMatrix4x4 startTransform,
const QVector3D &startPosition, const QVector3D &startLookAt,
const QVector3D &pressPos, const QVector3D &currentPos,
float zoomFactor)
{
QVector3D dragVector = currentPos - pressPos;
if (dragVector.length() < 0.001f)
return startLookAt;
const float *dataPtr(startTransform.data());
const QVector3D xAxis = QVector3D(dataPtr[0], dataPtr[1], dataPtr[2]).normalized();
const QVector3D yAxis = QVector3D(dataPtr[4], dataPtr[5], dataPtr[6]).normalized();
const QVector3D xDelta = -1.f * xAxis * dragVector.x();
const QVector3D yDelta = yAxis * dragVector.y();
const QVector3D delta = (xDelta + yDelta) * zoomFactor;
camera->setPosition(startPosition + delta);
return startLookAt + delta;
}
float GeneralHelper::zoomCamera(QQuick3DCamera *camera, float distance, float defaultLookAtDistance,
const QVector3D &lookAt, float zoomFactor, bool relative)
{
// Emprically determined divisor for nice zoom
float multiplier = 1.f + (distance / 40.f);
float newZoomFactor = relative ? qBound(.0001f, zoomFactor * multiplier, 10000.f)
: zoomFactor;
if (qobject_cast<QQuick3DOrthographicCamera *>(camera)) {
// Ortho camera we can simply scale
camera->setScale(QVector3D(newZoomFactor, newZoomFactor, newZoomFactor));
} else if (qobject_cast<QQuick3DPerspectiveCamera *>(camera)) {
// Perspective camera is zoomed by moving camera forward or backward while keeping the
// look-at point the same
const QVector3D lookAtVec = (camera->position() - lookAt).normalized();
const float newDistance = defaultLookAtDistance * newZoomFactor;
camera->setPosition(lookAt + (lookAtVec * newDistance));
}
return newZoomFactor;
}
}
}
#endif // QUICK3D_MODULE

View File

@@ -25,38 +25,43 @@
#pragma once
#include <QtCore/QObject>
#include <QtCore/QTimer>
#ifdef QUICK3D_MODULE
#include <QtQuick3D/private/qquick3dcamera_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qtimer.h>
namespace QmlDesigner {
namespace Internal {
class CameraControlHelper : public QObject
class GeneralHelper : public QObject
{
Q_OBJECT
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged)
public:
CameraControlHelper();
bool enabled();
void setEnabled(bool enabled);
GeneralHelper();
Q_INVOKABLE void requestOverlayUpdate();
Q_INVOKABLE QString generateUniqueName(const QString &nameRoot);
public slots:
void handleUpdateTimer();
Q_INVOKABLE void orbitCamera(QQuick3DCamera *camera, const QVector3D &startRotation,
const QVector3D &lookAtPoint, const QVector3D &pressPos,
const QVector3D &currentPos);
Q_INVOKABLE QVector3D panCamera(QQuick3DCamera *camera, const QMatrix4x4 startTransform,
const QVector3D &startPosition, const QVector3D &startLookAt,
const QVector3D &pressPos, const QVector3D &currentPos,
float zoomFactor);
Q_INVOKABLE float zoomCamera(QQuick3DCamera *camera, float distance,
float defaultLookAtDistance, const QVector3D &lookAt,
float zoomFactor, bool relative);
signals:
void updateInputs();
void enabledChanged(bool enabled);
void overlayUpdateNeeded();
private:
bool m_enabled = false;
QTimer m_inputUpdateTimer;
QTimer m_overlayUpdateTimer;
};
}
}
#endif // QUICK3D_MODULE

View File

@@ -37,7 +37,6 @@ namespace QmlDesigner {
namespace Internal {
MouseArea3D *MouseArea3D::s_mouseGrab = nullptr;
static const qreal s_mouseDragMultiplier = .02;
MouseArea3D::MouseArea3D(QQuick3DNode *parent)
: QQuick3DNode(parent)
@@ -340,7 +339,7 @@ qreal QmlDesigner::Internal::MouseArea3D::getNewRotationAngle(
dragDir = (screenDragDir - nodePos).normalized();
const QVector3D pressToCurrent = (currentPos - pressPos);
float magnitude = QVector3D::dotProduct(pressToCurrent, dragDir);
qreal angle = -s_mouseDragMultiplier * qreal(magnitude);
qreal angle = -mouseDragMultiplier() * qreal(magnitude);
return angle;
} else {
const QVector3D nodeToPress = (pressPos - nodePos).normalized();
@@ -397,7 +396,7 @@ void MouseArea3D::applyFreeRotation(QQuick3DNode *node, const QVector3D &startRo
QVector3D finalAxis = (dragVector.x() * yAxis + dragVector.y() * xAxis);
qreal degrees = qRadiansToDegrees(qreal(finalAxis.length()) * s_mouseDragMultiplier);
qreal degrees = qRadiansToDegrees(qreal(finalAxis.length()) * mouseDragMultiplier());
finalAxis.normalize();

View File

@@ -51,7 +51,7 @@ class MouseArea3D : public QQuick3DNode
Q_PROPERTY(bool hovering READ hovering NOTIFY hoveringChanged)
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(bool 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)
@@ -77,6 +77,8 @@ public:
qreal minAngle() const;
QQuick3DNode *pickNode() const;
static qreal mouseDragMultiplier() { return .02; }
public slots:
void setView3D(QQuick3DViewport *view3D);
void setGrabsMouse(bool grabsMouse);
@@ -157,7 +159,7 @@ private:
QVector3D getMousePosInPlane(const QPointF &mousePosInView) const;
static MouseArea3D *s_mouseGrab;
bool m_grabsMouse;
bool m_grabsMouse = false;
QVector3D m_mousePosInPlane;
QPointF m_circlePickArea;
qreal m_minAngle = 0.;

View File

@@ -62,7 +62,7 @@
#include <drop3dlibraryitemcommand.h>
#include "dummycontextobject.h"
#include "../editor3d/cameracontrolhelper.h"
#include "../editor3d/generalhelper.h"
#include "../editor3d/mousearea3d.h"
#include "../editor3d/camerageometry.h"
#include "../editor3d/gridgeometry.h"
@@ -104,10 +104,9 @@ bool Qt5InformationNodeInstanceServer::eventFilter(QObject *, QEvent *event)
QObject *Qt5InformationNodeInstanceServer::createEditView3D(QQmlEngine *engine)
{
auto helper = new QmlDesigner::Internal::CameraControlHelper();
engine->rootContext()->setContextProperty("designStudioNativeCameraControlHelper", helper);
#ifdef QUICK3D_MODULE
auto helper = new QmlDesigner::Internal::GeneralHelper();
engine->rootContext()->setContextProperty("_generalHelper", helper);
qmlRegisterType<QmlDesigner::Internal::MouseArea3D>("MouseArea3D", 1, 0, "MouseArea3D");
qmlRegisterType<QmlDesigner::Internal::CameraGeometry>("CameraGeometry", 1, 0, "CameraGeometry");
qmlRegisterType<QmlDesigner::Internal::GridGeometry>("GridGeometry", 1, 0, "GridGeometry");

View File

@@ -8,6 +8,7 @@
<file>mockfiles/GenericBackend.qml</file>
<file>mockfiles/Dialog.qml</file>
<file>mockfiles/EditView3D.qml</file>
<file>mockfiles/EditCameraController.qml</file>
<file>mockfiles/Arrow.qml</file>
<file>mockfiles/AutoScaleHelper.qml</file>
<file>mockfiles/MoveGizmo.qml</file>

View File

@@ -108,7 +108,7 @@ extend_qtc_executable(qml2puppet
extend_qtc_executable(qml2puppet
SOURCES_PREFIX "${SRCDIR}/qml2puppet/editor3d"
SOURCES
cameracontrolhelper.cpp cameracontrolhelper.h
generalhelper.cpp generalhelper.h
mousearea3d.cpp mousearea3d.h
camerageometry.cpp camerageometry.h
gridgeometry.cpp gridgeometry.h

View File

@@ -197,8 +197,8 @@ QtcTool {
"instances/qt5testnodeinstanceserver.h",
"instances/servernodeinstance.cpp",
"instances/servernodeinstance.h",
"editor3d/cameracontrolhelper.cpp",
"editor3d/cameracontrolhelper.h",
"editor3d/generalhelper.cpp",
"editor3d/generalhelper.h",
"editor3d/mousearea3d.cpp",
"editor3d/mousearea3d.h",
"editor3d/camerageometry.cpp",