QmlDesigner: Keep global transform on reparenting in navigator

By default, if user reparents a 3D node from under one 3D node to
another, the transform of the reparented node is now adjusted so that
after reparenting it will have the same global position, orientation,
and size as before.

This is only possible to do reliably if the old and new ancestor nodes
do not have both non-uniform scaling and rotation, as combination of
those can skew the transform into such as state that cannot be
replicated with simple three axis scaling. In case both non-uniform
scaling and rotation are present in the ancestral chains, the local
scaling will not be adjusted - just the position and rotation.

If reparented node is part of timeline animation in either new or old
location, transform is not adjusted.

If user holds down Alt key during drop, transform is not adjusted.

Since reparenting is done in base state, transform adjustment is
also always done in the base state.

Fixes: QDS-15392
Change-Id: Ibfda7459008aa0215d5cb368114f84a494e3e4b3
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Miikka Heikkinen
2025-05-19 17:56:23 +03:00
parent ba237401fd
commit 489d18385b
3 changed files with 363 additions and 22 deletions

View File

@@ -25,6 +25,7 @@
#include <nodehints.h> #include <nodehints.h>
#include <nodelistproperty.h> #include <nodelistproperty.h>
#include <nodeproperty.h> #include <nodeproperty.h>
#include <qml3dnode.h>
#include <qmldesignerconstants.h> #include <qmldesignerconstants.h>
#include <qmlitemnode.h> #include <qmlitemnode.h>
#include <rewritingexception.h> #include <rewritingexception.h>
@@ -169,6 +170,15 @@ static void reparentModelNodeToNodeProperty(NodeAbstractProperty &parentProperty
if (!scenePosition.isNull() && !qmlNode.isEffectItem()) if (!scenePosition.isNull() && !qmlNode.isEffectItem())
setScenePosition(modelNode, scenePosition); setScenePosition(modelNode, scenePosition);
} }
} else if (modelNode.metaInfo().isQtQuick3DNode()) {
Qml3DNode newParent3d(parentProperty.parentModelNode());
Qml3DNode node3d(modelNode);
if (!qApp->keyboardModifiers().testFlag(Qt::AltModifier)
&& !newParent3d.hasAnimatedTransform() && !node3d.hasAnimatedTransform()) {
node3d.reparentWithTransform(parentProperty);
} else {
parentProperty.reparentHere(modelNode);
}
} else { } else {
parentProperty.reparentHere(modelNode); parentProperty.reparentHere(modelNode);
} }

View File

@@ -2,23 +2,15 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "qml3dnode.h" #include "qml3dnode.h"
#include "auxiliarydataproperties.h"
#include "bindingproperty.h"
#include "nodehints.h"
#include "nodelistproperty.h"
#include "qmlanchors.h"
#include "qmlchangeset.h"
#include "variantproperty.h"
#include "plaintexteditmodifier.h" #include "qmltimelinekeyframegroup.h"
#include "rewriterview.h"
#include "modelmerger.h"
#include "rewritingexception.h"
#include <QUrl> #include <auxiliarydataproperties.h>
#include <QPlainTextEdit> #include <bindingproperty.h>
#include <QFileInfo> #include <nodeabstractproperty.h>
#include <QDir> #include <variantproperty.h>
#include <utils/qtcassert.h>
namespace QmlDesigner { namespace QmlDesigner {
@@ -111,4 +103,339 @@ QList<Qml3DNode> toQml3DNodeList(const QList<ModelNode> &modelNodeList)
return qml3DNodeList; return qml3DNodeList;
} }
QMatrix4x4 Qml3DNode::sceneTransform() const
{
if (modelNode().hasParentProperty()) {
Qml3DNode parentNode = modelNode().parentProperty().parentModelNode();
if (isValidQml3DNode(parentNode))
return parentNode.sceneTransform() * localTransform();
}
return localTransform();
}
static QVector3D vector3DFromString(const QString &s, bool *ok)
{
if (s.count(QLatin1Char(',')) != 2) {
if (ok)
*ok = false;
return {};
}
bool xGood, yGood, zGood;
int indexOpen = s.indexOf(QLatin1Char('('));
int indexClose = s.indexOf(QLatin1Char(')'));
if (indexClose == -1)
indexClose = s.length();
int index1 = s.indexOf(QLatin1Char(','));
int index2 = s.indexOf(QLatin1Char(','), index1 + 1);
qreal xCoord = s.mid(indexOpen + 1, index1 - indexOpen - 1).toDouble(&xGood);
qreal yCoord = s.mid(index1 + 1, index2 - index1 - 1).toDouble(&yGood);
qreal zCoord = s.mid(index2 + 1, indexClose - index2 - 1).toDouble(&zGood);
if (!xGood || !yGood || !zGood) {
if (ok)
*ok = false;
return QVector3D();
}
if (ok)
*ok = true;
return QVector3D(xCoord, yCoord, zCoord);
}
static QQuaternion quaternionFromString(const QString &s, bool *ok)
{
if (s.count(QLatin1Char(',')) != 3) {
if (ok)
*ok = false;
return {};
}
bool xGood, yGood, zGood, wGood;
int indexOpen = s.indexOf(QLatin1Char('('));
int indexClose = s.indexOf(QLatin1Char(')'));
if (indexClose == -1)
indexClose = s.length();
int index1 = s.indexOf(QLatin1Char(','));
int index2 = s.indexOf(QLatin1Char(','), index1 + 1);
int index3 = s.indexOf(QLatin1Char(','), index2 + 1);
qreal w = s.mid(indexOpen + 1, index1 - indexOpen - 1).toDouble(&wGood);
qreal x = s.mid(index1 + 1, index2 - index1 - 1).toDouble(&xGood);
qreal y = s.mid(index2 + 1, index3 - index2 - 1).toDouble(&yGood);
qreal z = s.mid(index3 + 1, indexClose - index3 - 1).toDouble(&zGood);
if (!xGood || !yGood || !zGood || !wGood) {
if (ok)
*ok = false;
return QQuaternion();
}
if (ok)
*ok = true;
return QQuaternion(w, x, y, z);
}
struct Exists {
bool x = false;
bool y = false;
bool z = false;
};
static float floatPropertyValue(const ModelNode &node, const PropertyName &propName, bool &exists)
{
auto prop = node.variantProperty(propName);
exists = prop.exists();
if (exists)
return prop.value().value<float>();
else
return 0.f;
};
static QVector3D vec3dPropertyValue(const ModelNode &node, const PropertyName &propName, Exists &exists)
{
auto prop = node.property(propName);
if (prop.exists()) {
exists = {true, true, true};
if (prop.isBindingProperty()) {
bool ok = false;
QVector3D vec = vector3DFromString(prop.toBindingProperty().expression(), &ok);
if (ok)
return vec;
return {};
}
return prop.toVariantProperty().value().value<QVector3D>();
} else {
QVector3D propVal;
propVal.setX(floatPropertyValue(node, propName + ".x", exists.x));
propVal.setY(floatPropertyValue(node, propName + ".y", exists.y));
propVal.setZ(floatPropertyValue(node, propName + ".z", exists.z));
return propVal;
}
};
QMatrix4x4 Qml3DNode::localTransform() const
{
if (!isValidQml3DNode(*this))
return {};
Exists exists;
QVector3D position = vec3dPropertyValue(modelNode(), "position", exists);
// Position can also be expressed by plain x, y, and z properties
if (!exists.x)
position.setX(floatPropertyValue(modelNode(), "x", exists.x));
if (!exists.y)
position.setY(floatPropertyValue(modelNode(), "y", exists.y));
if (!exists.z)
position.setZ(floatPropertyValue(modelNode(), "z", exists.z));
QQuaternion rotation;
auto rotProp = modelNode().property("rotation");
if (rotProp.exists()) {
if (rotProp.isBindingProperty()) {
bool ok = false;
QQuaternion q = quaternionFromString(rotProp.toBindingProperty().expression(), &ok);
if (ok)
rotation = q.normalized();
else
rotation = {};
} else {
rotation = rotProp.toVariantProperty().value().value<QQuaternion>().normalized();
}
} else {
QVector3D eulerRot = vec3dPropertyValue(modelNode(), "eulerRotation", exists);
rotation = QQuaternion::fromEulerAngles(eulerRot);
}
QVector3D scale = vec3dPropertyValue(modelNode(), "scale", exists);
if (!exists.x)
scale.setX(1.f);
if (!exists.y)
scale.setY(1.f);
if (!exists.z)
scale.setZ(1.f);
const QVector3D pivot = vec3dPropertyValue(modelNode(), "pivot", exists);
auto offset = (-pivot * scale);
QMatrix4x4 transform;
transform(0, 0) = scale[0];
transform(1, 1) = scale[1];
transform(2, 2) = scale[2];
transform(0, 3) = offset[0];
transform(1, 3) = offset[1];
transform(2, 3) = offset[2];
transform = QMatrix4x4{rotation.toRotationMatrix()} * transform;
transform(0, 3) += position[0];
transform(1, 3) += position[1];
transform(2, 3) += position[2];
return transform;
}
static void normalizeMatrix(QMatrix4x4 &m)
{
QVector4D c0 = m.column(0);
QVector4D c1 = m.column(1);
QVector4D c2 = m.column(2);
QVector4D c3 = m.column(3);
c0.normalize();
c1.normalize();
c2.normalize();
c3.normalize();
m.setColumn(0, c0);
m.setColumn(1, c1);
m.setColumn(2, c2);
m.setColumn(3, c3);
}
static QMatrix3x3 getUpper3x3(const QMatrix4x4 &m)
{
const float values[9] = {m(0, 0), m(0, 1), m(0, 2),
m(1, 0), m(1, 1), m(1, 2),
m(2, 0), m(2, 1), m(2, 2)};
return QMatrix3x3(values);
}
static QVector3D getPosition(const QMatrix4x4 &m)
{
return QVector3D(m(0, 3), m(1, 3), m(2, 3));
}
static QVector3D getScale(const QMatrix4x4 &m)
{
const float scaleX = m.column(0).length();
const float scaleY = m.column(1).length();
const float scaleZ = m.column(2).length();
return QVector3D(scaleX, scaleY, scaleZ);
}
static bool transformHasScalingAndRotation(const QMatrix4x4 &transform)
{
QVector3D scale = getScale(transform);
bool hasUniformScale = qFuzzyCompare(scale.x(), scale.y())
&& qFuzzyCompare(scale.y(), scale.z());
QVector3D xAxis = transform.column(0).toVector3D() / scale.x();
QVector3D yAxis = transform.column(1).toVector3D() / scale.y();
QVector3D zAxis = transform.column(2).toVector3D() / scale.z();
bool hasRotation = !qFuzzyCompare(xAxis, QVector3D(1.0f, 0.0f, 0.0f)) ||
!qFuzzyCompare(yAxis, QVector3D(0.0f, 1.0f, 0.0f)) ||
!qFuzzyCompare(zAxis, QVector3D(0.0f, 0.0f, 1.0f));
return !hasUniformScale && hasRotation;
}
bool Qml3DNode::hasAnimatedTransform()
{
QmlTimeline timeline = currentTimeline();
if (!timeline)
return false;
static const QSet<PropertyName> checkProps{
"position", "position.x", "position.y", "position.z", "x", "y", "z",
"rotation", "rotation.x", "rotation.y", "rotation.z", "rotation.w",
"eulerRotation", "eulerRotation.x", "eulerRotation.y", "eulerRotation.z",
"scale", "scale.x", "scale.y", "scale.z",
"pivot", "pivot.x", "pivot.y", "pivot.z"
};
for (ModelNode node = modelNode(); isValidQml3DNode(node); node = node.parentProperty().parentModelNode()) {
const QList<QmlTimelineKeyframeGroup> groups = timeline.keyframeGroupsForTarget(node);
if (std::ranges::any_of(groups, [&](const auto &group) {
return checkProps.contains(group.propertyName());
})) {
return true;
}
}
return false;
}
void Qml3DNode::setLocalTransform(const QMatrix4x4 &newParentSceneTransform,
const QMatrix4x4 &oldSceneTransform,
bool adjustScale)
{
QMatrix4x4 newLocalTransform = newParentSceneTransform.inverted() * oldSceneTransform;
QMatrix4x4 normalMatrix(newLocalTransform);
normalizeMatrix(normalMatrix);
QTC_ASSERT(!qFuzzyIsNull(normalMatrix.determinant()), return);
QQuaternion rotation = QQuaternion::fromRotationMatrix(getUpper3x3(normalMatrix)).normalized();
QVector3D position;
Exists exists;
QVector3D pivot = vec3dPropertyValue(modelNode(), "pivot", exists);
// Since reparenting always happens in base state, we also adjust transform in base state
auto setProp = [this](const PropertyName &propName, float value, float defaultValue) {
if (qFuzzyCompare(value, defaultValue))
modelNode().removeProperty(propName);
else
modelNode().variantProperty(propName).setValue(value);
};
const QVector3D scale = getScale(newLocalTransform);
if (adjustScale) {
position = getPosition(newLocalTransform) - rotation.rotatedVector(-pivot * scale);
modelNode().removeProperty("scale");
setProp("scale.x", scale.x(), 1.f);
setProp("scale.y", scale.y(), 1.f);
setProp("scale.z", scale.z(), 1.f);
} else {
QMatrix4x4 oldLocalTransform = localTransform();
QVector3D oldScale = getScale(oldLocalTransform);
position = getPosition(newLocalTransform)
- (rotation.rotatedVector(-pivot * oldScale));
}
modelNode().removeProperty("position");
modelNode().removeProperty("position.x");
modelNode().removeProperty("position.y");
modelNode().removeProperty("position.z");
modelNode().removeProperty("rotation");
modelNode().removeProperty("eulerRotation");
setProp("x", position.x(), 0.f);
setProp("y", position.y(), 0.f);
setProp("z", position.z(), 0.f);
const QVector3D eulerRotation = rotation.toEulerAngles();
setProp("eulerRotation.x", eulerRotation.x(), 0.f);
setProp("eulerRotation.y", eulerRotation.y(), 0.f);
setProp("eulerRotation.z", eulerRotation.z(), 0.f);
}
void Qml3DNode::reparentWithTransform(NodeAbstractProperty &parentProperty)
{
Qml3DNode oldParent3d;
if (modelNode().hasParentProperty())
oldParent3d = modelNode().parentProperty().parentModelNode();
Qml3DNode newParent3d(parentProperty.parentModelNode());
QMatrix4x4 oldParentSceneTransform = oldParent3d.sceneTransform();
QMatrix4x4 newParentSceneTransform = newParent3d.sceneTransform();
QMatrix4x4 oldSceneTransform = sceneTransform();
bool isNewScalable = !transformHasScalingAndRotation(newParentSceneTransform);
bool isOldScalable = !transformHasScalingAndRotation(oldParentSceneTransform);
parentProperty.reparentHere(modelNode());
if (oldParentSceneTransform != newParentSceneTransform) {
setLocalTransform(newParentSceneTransform, oldSceneTransform,
isNewScalable && isOldScalable);
}
}
} //QmlDesigner } //QmlDesigner

View File

@@ -3,15 +3,11 @@
#pragma once #pragma once
#include "qmlobjectnode.h"
#include "qmlstate.h"
#include "qmlvisualnode.h" #include "qmlvisualnode.h"
#include <modelnode.h>
#include <qmldesigner_global.h>
#include <QStringList> #include <modelnode.h>
#include <QRectF>
#include <QTransform> #include <QMatrix4x4>
namespace QmlDesigner { namespace QmlDesigner {
@@ -33,10 +29,18 @@ public:
bool handleEulerRotation(PropertyNameView name); bool handleEulerRotation(PropertyNameView name);
bool isBlocked(PropertyNameView propName) const; bool isBlocked(PropertyNameView propName) const;
void reparentWithTransform(NodeAbstractProperty &parentProperty);
bool hasAnimatedTransform();
friend auto qHash(const Qml3DNode &node) { return qHash(node.modelNode()); } friend auto qHash(const Qml3DNode &node) { return qHash(node.modelNode()); }
private: private:
void handleEulerRotationSet(); void handleEulerRotationSet();
QMatrix4x4 localTransform() const;
QMatrix4x4 sceneTransform() const;
void setLocalTransform(const QMatrix4x4 &newParentSceneTransform,
const QMatrix4x4 &oldSceneTransform,
bool adjustScale);
}; };
QMLDESIGNER_EXPORT QList<ModelNode> toModelNodeList(const QList<Qml3DNode> &fxItemNodeList); QMLDESIGNER_EXPORT QList<ModelNode> toModelNodeList(const QList<Qml3DNode> &fxItemNodeList);