QmlDesigner: Add fly mode for 3D view

You can now activate fly mode in 3D view by pressing right mouse
button. In fly mode, cursor is hidden and mouse controls edit camera
rotation directly, and WASDQE can be used to move the camera around.

Fixes: QDS-12030
Change-Id: I52550502632af19de36a1557d9aac84ff3cb18cc
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
This commit is contained in:
Miikka Heikkinen
2024-03-01 15:41:19 +02:00
parent af9ce82245
commit 74761b0e64
12 changed files with 439 additions and 51 deletions

View File

@@ -26,8 +26,9 @@ Item {
readonly property vector3d _defaultCameraPosition: Qt.vector3d(0, 600, 600)
readonly property vector3d _defaultCameraRotation: Qt.vector3d(-45, 0, 0)
readonly property real _defaultCameraLookAtDistance: _defaultCameraPosition.length()
readonly property real _keyPanAmount: 5
readonly property real _keyPanAmount: 10
property bool ignoreToolState: false
property bool flyMode: viewRoot.flyMode
z: 10
anchors.fill: parent
@@ -150,6 +151,58 @@ Item {
_lookAtPoint, _zoomFactor, true);
}
function rotateCamera(angles)
{
cameraCtrl._lookAtPoint = _generalHelper.rotateCamera(camera, angles, _lookAtPoint);
}
function moveCamera(moveVec)
{
cameraCtrl._lookAtPoint = _generalHelper.moveCamera(camera, _lookAtPoint, _zoomFactor,
moveVec);
}
function getMoveVectorForKey(key) {
if (flyMode) {
switch (key) {
case Qt.Key_A:
case Qt.Key_Left:
return Qt.vector3d(_keyPanAmount, 0, 0);
case Qt.Key_D:
case Qt.Key_Right:
return Qt.vector3d(-_keyPanAmount, 0, 0);
case Qt.Key_E:
case Qt.Key_PageUp:
return Qt.vector3d(0, _keyPanAmount, 0);
case Qt.Key_Q:
case Qt.Key_PageDown:
return Qt.vector3d(0, -_keyPanAmount, 0);
case Qt.Key_W:
case Qt.Key_Up:
return Qt.vector3d(0, 0, _keyPanAmount);
case Qt.Key_S:
case Qt.Key_Down:
return Qt.vector3d(0, 0, -_keyPanAmount);
default:
break;
}
} else {
switch (key) {
case Qt.Key_Left:
return Qt.vector3d(_keyPanAmount, 0, 0);
case Qt.Key_Right:
return Qt.vector3d(-_keyPanAmount, 0, 0);
case Qt.Key_Up:
return Qt.vector3d(0, _keyPanAmount, 0);
case Qt.Key_Down:
return Qt.vector3d(0, -_keyPanAmount, 0);
default:
break;
}
}
return Qt.vector3d(0, 0, 0);
}
onCameraChanged: {
if (camera && _prevCamera) {
// Reset zoom on previous camera to ensure it's properties are good to copy to new cam
@@ -166,6 +219,25 @@ Item {
_prevCamera = camera;
}
onFlyModeChanged: {
if (cameraCtrl._dragging) {
cameraCtrl._dragging = false;
cameraCtrl.storeCameraState(0);
}
_generalHelper.stopAllCameraMoves()
}
Connections {
target: _generalHelper
enabled: viewRoot.activeSplit === cameraCtrl.splitId
function onRequestCameraMove(camera, moveVec) {
if (camera === cameraCtrl.camera) {
cameraCtrl.moveCamera(moveVec);
_generalHelper.requestRender();
}
}
}
MouseArea {
id: mouseHandler
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
@@ -190,6 +262,8 @@ Item {
}
}
onPressed: (mouse) => {
if (cameraCtrl.flyMode)
return;
viewRoot.activeSplit = cameraCtrl.splitId
if (cameraCtrl.camera && mouse.modifiers === Qt.AltModifier) {
cameraCtrl._dragging = true;
@@ -215,6 +289,8 @@ Item {
onCanceled: handleRelease()
onWheel: (wheel) => {
if (cameraCtrl.flyMode && cameraCtrl.splitId !== viewRoot.activeSplit)
return;
viewRoot.activeSplit = cameraCtrl.splitId
if (cameraCtrl.camera) {
// Empirically determined divisor for nice zoom
@@ -225,33 +301,13 @@ Item {
}
Keys.onPressed: (event) => {
var pressPoint = Qt.vector3d(view3d.width / 2, view3d.height / 2, 0);
var currentPoint;
event.accepted = true;
_generalHelper.startCameraMove(cameraCtrl.camera, cameraCtrl.getMoveVectorForKey(event.key));
}
switch (event.key) {
case Qt.Key_Left:
currentPoint = pressPoint.plus(Qt.vector3d(_keyPanAmount, 0, 0));
break;
case Qt.Key_Right:
currentPoint = pressPoint.plus(Qt.vector3d(-_keyPanAmount, 0, 0));
break;
case Qt.Key_Up:
currentPoint = pressPoint.plus(Qt.vector3d(0, _keyPanAmount, 0));
break;
case Qt.Key_Down:
currentPoint = pressPoint.plus(Qt.vector3d(0, -_keyPanAmount, 0));
break;
default:
break;
}
if (currentPoint) {
_lookAtPoint = _generalHelper.panCamera(
camera, cameraCtrl.camera.sceneTransform,
cameraCtrl.camera.position, _lookAtPoint,
pressPoint, currentPoint, _zoomFactor);
event.accepted = true;
}
Keys.onReleased: (event) => {
event.accepted = true;
_generalHelper.stopCameraMove(cameraCtrl.getMoveVectorForKey(event.key));
}
OriginGizmo {

View File

@@ -37,6 +37,7 @@ Item {
property color gridColor: "#cccccc"
property bool syncEnvBackground: false
property bool splitView: false
property bool flyMode: false
enum SelectionMode { Item, Group }
enum TransformMode { Move, Rotate, Scale }
@@ -129,6 +130,7 @@ Item {
selectionBoxCount = 0;
editViewsChanged();
cameraControls[activeSplit].forceActiveFocus();
return true;
}
return false;
@@ -351,6 +353,11 @@ Item {
cameraControls[i].restoreDefaultState();
}
if ("flyMode" in toolStates)
flyMode = toolStates.flyMode;
else if (resetToDefault)
flyMode = false;
if ("splitView" in toolStates)
splitView = toolStates.splitView;
else if (resetToDefault)
@@ -598,6 +605,16 @@ Item {
return activeOverlayView.gizmoAt(splitPoint.x, splitPoint.y);
}
function rotateEditCamera(angles)
{
cameraControls[activeSplit].rotateCamera(angles);
}
function moveEditCamera(amounts)
{
cameraControls[activeSplit].moveCamera(amounts);
}
Component.onCompleted: {
createEditViews();
selectObjects([]);
@@ -822,6 +839,9 @@ Item {
property bool initialMoveBlock: false
onPressed: (mouse) => {
if (viewRoot.flyMode)
return;
viewRoot.updateActiveSplit(mouse.x, mouse.y);
let splitPoint = viewRoot.resolveSplitPoint(mouse.x, mouse.y);

View File

@@ -66,6 +66,11 @@ GeneralHelper::GeneralHelper()
QList<QColor> defaultBg;
defaultBg.append(QColor());
m_bgColor = QVariant::fromValue(defaultBg);
m_camMoveData.timer.setInterval(16);
QObject::connect(&m_camMoveData.timer, &QTimer::timeout, this, [this]() {
emit requestCameraMove(m_camMoveData.camera, m_camMoveData.combinedMoveVector);
});
}
void GeneralHelper::requestOverlayUpdate()
@@ -142,14 +147,111 @@ QVector3D GeneralHelper::panCamera(QQuick3DCamera *camera, const QMatrix4x4 star
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 xDelta = xAxis * dragVector.x();
const QVector3D yDelta = yAxis * dragVector.y();
const QVector3D delta = (xDelta + yDelta) * zoomFactor;
const QVector3D delta = (yDelta - xDelta) * zoomFactor;
camera->setPosition(startPosition + delta);
return startLookAt + delta;
}
// Moves camera in 3D space and returns new look-at point
QVector3D GeneralHelper::moveCamera(QQuick3DCamera *camera, const QVector3D &startLookAt,
float zoomFactor, const QVector3D &moveVector)
{
if (moveVector.length() < 0.001f)
return startLookAt;
QMatrix4x4 m = camera->sceneTransform(); // Works because edit camera is at scene root
const float *dataPtr(m.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 zAxis = QVector3D(dataPtr[8], dataPtr[9], dataPtr[10]).normalized();
const QVector3D xDelta = xAxis * moveVector.x();
const QVector3D yDelta = yAxis * moveVector.y();
const QVector3D zDelta = zAxis * moveVector.z();
const QVector3D delta = (yDelta - xDelta - zDelta) * zoomFactor;
camera->setPosition(camera->position() + delta);
return startLookAt + delta;
}
// Rotates camera and returns the new look-at point
QVector3D GeneralHelper::rotateCamera(QQuick3DCamera *camera, const QPointF &angles,
const QVector3D &lookAtPoint)
{
float lookAtDist = (camera->scenePosition() - lookAtPoint).length();
if (qAbs(angles.y()) > 0.001f)
camera->rotate(angles.y(), QVector3D(1.f, 0.f, 0.f), QQuick3DNode::LocalSpace);
// Rotation around Y-axis is done in scene space to keep horizon level
if (qAbs(angles.x()) > 0.001f)
camera->rotate(angles.x(), QVector3D(0.f, 1.f, 0.f), QQuick3DNode::SceneSpace);
QMatrix4x4 m = camera->sceneTransform();
const float *dataPtr(m.data());
QVector3D newLookVector(dataPtr[8], dataPtr[9], dataPtr[10]);
newLookVector.normalize();
newLookVector *= lookAtDist;
return camera->scenePosition() - newLookVector;
}
void GeneralHelper::updateCombinedCameraMoveVector()
{
QVector3D combinedVec;
for (const QVector3D &vec : std::as_const(m_camMoveData.moveVectors))
combinedVec += vec;
m_camMoveData.combinedMoveVector = combinedVec;
}
// Key events can be buffered and there are repeat delays imposed by OS, so to get smooth camera
// movement in response to keys, register start/stop of moves along each axis and use timer to
// trigger new moves along registered axes.
void GeneralHelper::startCameraMove(QQuick3DCamera *camera, const QVector3D moveVector)
{
if (moveVector.isNull())
return;
if (m_camMoveData.camera != camera) {
m_camMoveData.camera = camera;
m_camMoveData.moveVectors.clear();
}
if (!m_camMoveData.moveVectors.contains(moveVector)) {
m_camMoveData.moveVectors.append(moveVector);
updateCombinedCameraMoveVector();
}
if (!m_camMoveData.timer.isActive()) {
m_camMoveData.timer.start();
emit requestCameraMove(camera, m_camMoveData.combinedMoveVector);
}
}
void GeneralHelper::stopCameraMove(const QVector3D moveVector)
{
if (moveVector.isNull())
return;
m_camMoveData.moveVectors.removeOne(moveVector);
updateCombinedCameraMoveVector();
if (m_camMoveData.moveVectors.isEmpty())
m_camMoveData.timer.stop();
}
void GeneralHelper::stopAllCameraMoves()
{
m_camMoveData.moveVectors.clear();
m_camMoveData.combinedMoveVector = {};
m_camMoveData.timer.stop();
}
float GeneralHelper::zoomCamera([[maybe_unused]] QQuick3DViewport *viewPort,
QQuick3DCamera *camera,
float distance,

View File

@@ -10,6 +10,7 @@
#include <QMatrix4x4>
#include <QObject>
#include <QPointer>
#include <QPointF>
#include <QQuaternion>
#include <QTimer>
#include <QUrl>
@@ -52,6 +53,15 @@ public:
const QVector3D &startPosition, const QVector3D &startLookAt,
const QVector3D &pressPos, const QVector3D &currentPos,
float zoomFactor);
Q_INVOKABLE QVector3D moveCamera(QQuick3DCamera *camera,const QVector3D &startLookAt,
float zoomFactor, const QVector3D &moveVector);
Q_INVOKABLE QVector3D rotateCamera(QQuick3DCamera *camera, const QPointF &angles,
const QVector3D &lookAtPoint);
Q_INVOKABLE void startCameraMove(QQuick3DCamera *camera, const QVector3D moveVector);
Q_INVOKABLE void stopCameraMove(const QVector3D moveVector);
Q_INVOKABLE void stopAllCameraMoves();
Q_INVOKABLE float zoomCamera(QQuick3DViewport *viewPort, QQuick3DCamera *camera, float distance,
float defaultLookAtDistance, const QVector3D &lookAt,
float zoomFactor, bool relative);
@@ -149,6 +159,8 @@ signals:
void minGridStepChanged();
void updateDragTooltip();
void sceneEnvDataChanged();
void requestCameraMove(QQuick3DCamera *camera, const QVector3D &moveVector);
void requestRender();
private:
void handlePendingToolStateUpdate();
@@ -163,6 +175,15 @@ private:
QHash<QString, QVariantMap> m_toolStates;
QHash<QString, QVariantMap> m_toolStatesPending;
QSet<QQuick3DNode *> m_rotationBlockedNodes;
void updateCombinedCameraMoveVector();
struct CameraMoveKeyData {
QQuick3DCamera *camera;
QList<QVector3D> moveVectors;
QVector3D combinedMoveVector;
QTimer timer;
};
CameraMoveKeyData m_camMoveData;
struct SceneEnvData {
QQuick3DSceneEnvironment::QQuick3DEnvironmentBackgroundTypes backgroundMode;

View File

@@ -254,8 +254,11 @@ void Qt5InformationNodeInstanceServer::handleInputEvents()
QGuiApplication::sendEvent(m_editView3DData.window, &me);
// Context menu requested
if (command.button() == Qt::RightButton && command.modifiers() == Qt::NoModifier)
if (command.type() == QEvent::MouseButtonPress
&& command.buttons() == Qt::RightButton
&& command.modifiers() == Qt::NoModifier) {
getNodeAtPos(command.pos());
}
}
}
@@ -488,6 +491,9 @@ void Qt5InformationNodeInstanceServer::createEditView3D()
auto helper = new QmlDesigner::Internal::GeneralHelper();
QObject::connect(helper, &QmlDesigner::Internal::GeneralHelper::toolStateChanged,
this, &Qt5InformationNodeInstanceServer::handleToolStateChanged);
QObject::connect(helper, &QmlDesigner::Internal::GeneralHelper::requestRender, this, [this]() {
render3DEditView(1);
});
engine()->rootContext()->setContextProperty("_generalHelper", helper);
engine()->addImageProvider(QLatin1String("IconGizmoImageProvider"),
new QmlDesigner::Internal::IconGizmoImageProvider);
@@ -2417,6 +2423,7 @@ void Qt5InformationNodeInstanceServer::view3DAction(const View3DActionCommand &c
if (!m_editView3DSetupDone)
return;
#ifdef QUICK3D_MODULE
QVariantMap updatedToolState;
QVariantMap updatedViewState;
int renderCount = 1;
@@ -2498,16 +2505,31 @@ void Qt5InformationNodeInstanceServer::view3DAction(const View3DActionCommand &c
case View3DActionType::ParticlesSeek:
m_particleAnimationDriver->setSeekerPosition(command.position());
break;
#endif
#ifdef QUICK3D_MODULE
#endif // QUICK3D_PARTICLES_MODULE
case View3DActionType::GetNodeAtPos: {
getNodeAtPos(command.value().toPointF());
return;
}
#endif
case View3DActionType::SplitViewToggle:
updatedToolState.insert("splitView", command.isEnabled());
break;
case View3DActionType::FlyModeToggle:
updatedToolState.insert("flyMode", command.isEnabled());
break;
case View3DActionType::EditCameraRotation:
QMetaObject::invokeMethod(m_editView3DData.rootItem, "rotateEditCamera",
Q_ARG(QVariant, command.value()));
break;
case View3DActionType::EditCameraMove:
QMetaObject::invokeMethod(m_editView3DData.rootItem, "moveEditCamera",
Q_ARG(QVariant, command.value()));
break;
case View3DActionType::EditCameraStopAllMoves: {
auto helper = qobject_cast<QmlDesigner::Internal::GeneralHelper *>(m_3dHelper);
if (helper)
emit helper->stopAllCameraMoves();
break;
}
case View3DActionType::ShowWireframe:
updatedToolState.insert("showWireframe", command.value().toList());
break;
@@ -2531,6 +2553,7 @@ void Qt5InformationNodeInstanceServer::view3DAction(const View3DActionCommand &c
}
render3DEditView(renderCount);
#endif // QUICK3D_MODULE
}
void Qt5InformationNodeInstanceServer::requestModelNodePreviewImage(const RequestModelNodePreviewImageCommand &command)