QmlDesigner: Implement group selection boxes in 3D edit view

Object's selection box now includes the bounds of all of its
descendants. Selection boxes of immediate children of a selected
object are also drawn.
Individual/group selection buttons also now work as expected.

Change-Id: Ice7ef9a536e32c6bb6da70fe23bf0a38e72c14f8
Fixes: QDS-1210
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Miikka Heikkinen
2019-11-21 17:37:09 +02:00
parent 9dac42f153
commit 389f96b0ee
7 changed files with 212 additions and 67 deletions

View File

@@ -55,9 +55,14 @@ Window {
selectedNode = object;
}
function emitObjectClicked(object) {
selectObject(object);
objectClicked(object);
function handleObjectClicked(object) {
var theObject = object;
if (btnSelectGroup.selected) {
while (theObject && theObject.parent !== scene)
theObject = theObject.parent;
}
selectObject(theObject);
objectClicked(theObject);
}
function addLightGizmo(obj)
@@ -68,7 +73,7 @@ Window {
{"view3D": overlayView, "targetNode": obj,
"selectedNode": selectedNode});
lightGizmos[lightGizmos.length] = gizmo;
gizmo.clicked.connect(emitObjectClicked);
gizmo.clicked.connect(handleObjectClicked);
gizmo.selectedNode = Qt.binding(function() {return selectedNode;});
}
}
@@ -83,7 +88,7 @@ Window {
{"view3D": overlayView, "targetNode": obj, "geometryName": geometryName,
"viewPortRect": viewPortRect, "selectedNode": selectedNode});
cameraGizmos[cameraGizmos.length] = gizmo;
gizmo.clicked.connect(emitObjectClicked);
gizmo.clicked.connect(handleObjectClicked);
gizmo.viewPortRect = Qt.binding(function() {return viewPortRect;});
gizmo.selectedNode = Qt.binding(function() {return selectedNode;});
}
@@ -178,7 +183,7 @@ Window {
onTapped: {
var pickResult = editView.pick(eventPoint.scenePosition.x,
eventPoint.scenePosition.y);
emitObjectClicked(pickResult.objectHit);
handleObjectClicked(pickResult.objectHit);
}
}

View File

@@ -52,7 +52,7 @@ Node {
orientation: selectionBox.targetNode ? selectionBox.targetNode.orientation : Node.LeftHanded
rotationOrder: selectionBox.targetNode ? selectionBox.targetNode.rotationOrder : Node.YXZ
visible: selectionBox.targetNode && selectionBox.targetNode instanceof Model
visible: selectionBox.targetNode && !selectionBoxGeometry.isEmpty
materials: [
DefaultMaterial {

View File

@@ -29,15 +29,17 @@
#include <QtQuick3D/private/qquick3dorthographiccamera_p.h>
#include <QtQuick3D/private/qquick3dperspectivecamera_p.h>
#include <QtQuick3D/private/qquick3dobject_p_p.h>
#include <QtQuick3D/private/qquick3dcamera_p.h>
#include <QtQuick3D/private/qquick3dnode_p.h>
#include <QtQuick3D/private/qquick3dmodel_p.h>
#include <QtQuick3D/private/qquick3dviewport_p.h>
#include <QtQuick3D/private/qquick3ddefaultmaterial_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendercontextcore_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendermodel_p.h>
#include <QtQuick3DUtils/private/qssgbounds3_p.h>
#include <QtQuick/qquickwindow.h>
#include <QtCore/qhash.h>
#include <QtCore/qmath.h>
#include <QtGui/qmatrix4x4.h>
namespace QmlDesigner {
namespace Internal {

View File

@@ -27,14 +27,21 @@
#ifdef QUICK3D_MODULE
#include <QtQuick3D/private/qquick3dcamera_p.h>
#include <QtQuick3D/private/qquick3dnode_p.h>
#include <QtQuick3D/private/qquick3dviewport_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qtimer.h>
#include <QtCore/qhash.h>
#include <QtGui/qvector3d.h>
#include <QtGui/qmatrix4x4.h>
QT_BEGIN_NAMESPACE
class QQuick3DCamera;
class QQuick3DNode;
class QQuick3DViewport;
QT_END_NAMESPACE
namespace QmlDesigner {
namespace Internal {
class GeneralHelper : public QObject
{
Q_OBJECT

View File

@@ -35,6 +35,9 @@
#include <QtQuick3D/private/qquick3dmodel_p.h>
#include <QtQuick3D/private/qquick3dobject_p_p.h>
#include <QtQuick/qquickwindow.h>
#include <QtCore/qvector.h>
#include <limits>
namespace QmlDesigner {
namespace Internal {
@@ -46,6 +49,9 @@ SelectionBoxGeometry::SelectionBoxGeometry()
SelectionBoxGeometry::~SelectionBoxGeometry()
{
for (auto &connection : qAsConst(m_connections))
QObject::disconnect(connection);
m_connections.clear();
}
QQuick3DNode *SelectionBoxGeometry::targetNode() const
@@ -63,6 +69,11 @@ QQuick3DViewport *SelectionBoxGeometry::view3D() const
return m_view3D;
}
bool QmlDesigner::Internal::SelectionBoxGeometry::isEmpty() const
{
return m_isEmpty;
}
void SelectionBoxGeometry::setTargetNode(QQuick3DNode *targetNode)
{
if (m_targetNode == targetNode)
@@ -111,13 +122,18 @@ QSSGRenderGraphObject *SelectionBoxGeometry::updateSpatialNode(QSSGRenderGraphOb
QSSGRenderGeometry *geometry = static_cast<QSSGRenderGeometry *>(node);
geometry->clear();
for (auto &connection : qAsConst(m_connections))
QObject::disconnect(connection);
m_connections.clear();
QByteArray vertexData;
QByteArray indexData;
QVector3D minBounds(-100.f, -100.f, -100.f);
QVector3D maxBounds(100.f, 100.f, 100.f);
QVector3D extents;
static const float floatMin = std::numeric_limits<float>::lowest();
static const float floatMax = std::numeric_limits<float>::max();
QVector3D minBounds = QVector3D(floatMax, floatMax, floatMax);
QVector3D maxBounds = QVector3D(floatMin, floatMin, floatMin);
if (m_targetNode) {
auto rootPriv = QQuick3DObjectPrivate::get(m_rootNode);
@@ -134,33 +150,12 @@ QSSGRenderGraphObject *SelectionBoxGeometry::updateSpatialNode(QSSGRenderGraphOb
rootRN->markDirty(QSSGRenderNode::TransformDirtyFlag::TransformNotDirty);
rootRN->calculateGlobalVariables();
}
if (auto modelNode = qobject_cast<QQuick3DModel *>(m_targetNode)) {
auto nodePriv = QQuick3DObjectPrivate::get(m_targetNode);
if (auto renderModel = static_cast<QSSGRenderModel *>(nodePriv->spatialNode)) {
QWindow *window = static_cast<QWindow *>(m_view3D->window());
if (window) {
auto context = QSSGRenderContextInterface::getRenderContextInterface(
quintptr(window));
if (!context.isNull()) {
auto bufferManager = context->bufferManager();
QSSGBounds3 bounds = renderModel->getModelBounds(bufferManager);
QVector3D center = bounds.center();
extents = bounds.extents();
minBounds = center - extents;
maxBounds = center + extents;
}
}
}
}
getBounds(m_targetNode, vertexData, indexData, minBounds, maxBounds, QMatrix4x4());
} else {
// Fill some dummy data so geometry won't get rejected
appendVertexData(vertexData, indexData, minBounds, maxBounds);
}
// Adjust bounds to reduce targetNode pixels obscuring the selection box
extents /= 1000.f;
minBounds -= extents;
maxBounds += extents;
fillVertexData(vertexData, indexData, minBounds, maxBounds);
geometry->addAttribute(QSSGRenderGeometry::Attribute::PositionSemantic, 0,
QSSGRenderGeometry::Attribute::ComponentType::F32Type);
geometry->addAttribute(QSSGRenderGeometry::Attribute::IndexSemantic, 0,
@@ -171,19 +166,138 @@ QSSGRenderGraphObject *SelectionBoxGeometry::updateSpatialNode(QSSGRenderGraphOb
geometry->setPrimitiveType(QSSGRenderGeometry::Lines);
geometry->setBounds(minBounds, maxBounds);
bool empty = minBounds.isNull() && maxBounds.isNull();
if (m_isEmpty != empty) {
m_isEmpty = empty;
// Delay notification until we're done with spatial node updates
QTimer::singleShot(0, this, &SelectionBoxGeometry::isEmptyChanged);
}
return node;
}
void SelectionBoxGeometry::fillVertexData(QByteArray &vertexData, QByteArray &indexData,
const QVector3D &minBounds, const QVector3D &maxBounds)
void SelectionBoxGeometry::getBounds(QQuick3DNode *node, QByteArray &vertexData,
QByteArray &indexData, QVector3D &minBounds,
QVector3D &maxBounds, const QMatrix4x4 &transform)
{
const int vertexSize = int(sizeof(float)) * 8 * 3; // 8 vertices, 3 floats/vert
vertexData.resize(vertexSize);
const int indexSize = int(sizeof(quint16)) * 12 * 2; // 12 lines, 2 vert/line
indexData.resize(indexSize);
QMatrix4x4 fullTransform;
auto nodePriv = QQuick3DObjectPrivate::get(node);
auto renderNode = static_cast<QSSGRenderNode *>(nodePriv->spatialNode);
auto dataPtr = reinterpret_cast<float *>(vertexData.data());
auto indexPtr = reinterpret_cast<quint16 *>(indexData.data());
// All transforms are relative to targetNode transform, so its local transform is ignored
if (node != m_targetNode) {
if (renderNode) {
if (renderNode->flags.testFlag(QSSGRenderNode::Flag::TransformDirty))
renderNode->calculateLocalTransform();
fullTransform = transform * renderNode->localTransform;
}
m_connections << QObject::connect(node, &QQuick3DNode::scaleChanged,
this, &SelectionBoxGeometry::update, Qt::QueuedConnection);
m_connections << QObject::connect(node, &QQuick3DNode::rotationChanged,
this, &SelectionBoxGeometry::update, Qt::QueuedConnection);
m_connections << QObject::connect(node, &QQuick3DNode::positionChanged,
this, &SelectionBoxGeometry::update, Qt::QueuedConnection);
m_connections << QObject::connect(node, &QQuick3DNode::pivotChanged,
this, &SelectionBoxGeometry::update, Qt::QueuedConnection);
m_connections << QObject::connect(node, &QQuick3DNode::orientationChanged,
this, &SelectionBoxGeometry::update, Qt::QueuedConnection);
m_connections << QObject::connect(node, &QQuick3DNode::rotationOrderChanged,
this, &SelectionBoxGeometry::update, Qt::QueuedConnection);
}
QVector<QVector3D> minBoundsVec;
QVector<QVector3D> maxBoundsVec;
// Check for children
const auto children = node->childItems();
for (const auto child : children) {
if (auto childNode = qobject_cast<QQuick3DNode *>(child)) {
QVector3D newMinBounds = minBounds;
QVector3D newMaxBounds = maxBounds;
getBounds(childNode, vertexData, indexData, newMinBounds, newMaxBounds, fullTransform);
minBoundsVec << newMinBounds;
maxBoundsVec << newMaxBounds;
}
}
// Combine all child bounds
for (const auto &newBounds : qAsConst(minBoundsVec)) {
minBounds.setX(qMin(newBounds.x(), minBounds.x()));
minBounds.setY(qMin(newBounds.y(), minBounds.y()));
minBounds.setZ(qMin(newBounds.z(), minBounds.z()));
}
for (const auto &newBounds : qAsConst(maxBoundsVec)) {
maxBounds.setX(qMax(newBounds.x(), maxBounds.x()));
maxBounds.setY(qMax(newBounds.y(), maxBounds.y()));
maxBounds.setZ(qMax(newBounds.z(), maxBounds.z()));
}
if (auto modelNode = qobject_cast<QQuick3DModel *>(node)) {
if (auto renderModel = static_cast<QSSGRenderModel *>(renderNode)) {
QWindow *window = static_cast<QWindow *>(m_view3D->window());
if (window) {
auto context = QSSGRenderContextInterface::getRenderContextInterface(
quintptr(window));
if (!context.isNull()) {
auto bufferManager = context->bufferManager();
QSSGBounds3 bounds = renderModel->getModelBounds(bufferManager);
QVector3D center = bounds.center();
QVector3D extents = bounds.extents();
QVector3D localMin = center - extents;
QVector3D localMax = center + extents;
// Transform all corners of the local bounding box to find final extent in
// in parent space
auto checkCorner = [&minBounds, &maxBounds, &fullTransform]
(const QVector3D &corner) {
QVector3D mappedCorner = fullTransform.map(corner);
minBounds.setX(qMin(mappedCorner.x(), minBounds.x()));
minBounds.setY(qMin(mappedCorner.y(), minBounds.y()));
minBounds.setZ(qMin(mappedCorner.z(), minBounds.z()));
maxBounds.setX(qMax(mappedCorner.x(), maxBounds.x()));
maxBounds.setY(qMax(mappedCorner.y(), maxBounds.y()));
maxBounds.setZ(qMax(mappedCorner.z(), maxBounds.z()));
};
checkCorner(localMin);
checkCorner(localMax);
checkCorner(QVector3D(localMin.x(), localMin.y(), localMax.z()));
checkCorner(QVector3D(localMin.x(), localMax.y(), localMin.z()));
checkCorner(QVector3D(localMax.x(), localMin.y(), localMin.z()));
checkCorner(QVector3D(localMin.x(), localMax.y(), localMax.z()));
checkCorner(QVector3D(localMax.x(), localMax.y(), localMin.z()));
checkCorner(QVector3D(localMax.x(), localMin.y(), localMax.z()));
}
}
}
}
// Target node and immediate children get selection boxes
if (transform.isIdentity()) {
// Adjust bounds to reduce targetNode pixels obscuring the selection box
QVector3D extents = (maxBounds - minBounds) / 1000.f;
QVector3D minAdjBounds = minBounds - extents;
QVector3D maxAdjBounds = maxBounds + extents;
appendVertexData(vertexData, indexData, minAdjBounds, maxAdjBounds);
}
}
void SelectionBoxGeometry::appendVertexData(QByteArray &vertexData, QByteArray &indexData,
const QVector3D &minBounds, const QVector3D &maxBounds)
{
int initialVertexSize = vertexData.size();
int initialIndexSize = indexData.size();
const int vertexSize = int(sizeof(float)) * 8 * 3; // 8 vertices, 3 floats/vert
quint16 indexAdd = quint16(initialVertexSize / 12);
vertexData.resize(initialVertexSize + vertexSize);
const int indexSize = int(sizeof(quint16)) * 12 * 2; // 12 lines, 2 vert/line
indexData.resize(initialIndexSize + indexSize);
auto dataPtr = reinterpret_cast<float *>(vertexData.data() + initialVertexSize);
auto indexPtr = reinterpret_cast<quint16 *>(indexData.data() + initialIndexSize);
*dataPtr++ = maxBounds.x(); *dataPtr++ = maxBounds.y(); *dataPtr++ = maxBounds.z();
*dataPtr++ = minBounds.x(); *dataPtr++ = maxBounds.y(); *dataPtr++ = maxBounds.z();
@@ -194,20 +308,20 @@ void SelectionBoxGeometry::fillVertexData(QByteArray &vertexData, QByteArray &in
*dataPtr++ = minBounds.x(); *dataPtr++ = minBounds.y(); *dataPtr++ = minBounds.z();
*dataPtr++ = maxBounds.x(); *dataPtr++ = minBounds.y(); *dataPtr++ = minBounds.z();
*indexPtr++ = 0; *indexPtr++ = 1;
*indexPtr++ = 1; *indexPtr++ = 2;
*indexPtr++ = 2; *indexPtr++ = 3;
*indexPtr++ = 3; *indexPtr++ = 0;
*indexPtr++ = 0 + indexAdd; *indexPtr++ = 1 + indexAdd;
*indexPtr++ = 1 + indexAdd; *indexPtr++ = 2 + indexAdd;
*indexPtr++ = 2 + indexAdd; *indexPtr++ = 3 + indexAdd;
*indexPtr++ = 3 + indexAdd; *indexPtr++ = 0 + indexAdd;
*indexPtr++ = 0; *indexPtr++ = 4;
*indexPtr++ = 1; *indexPtr++ = 5;
*indexPtr++ = 2; *indexPtr++ = 6;
*indexPtr++ = 3; *indexPtr++ = 7;
*indexPtr++ = 0 + indexAdd; *indexPtr++ = 4 + indexAdd;
*indexPtr++ = 1 + indexAdd; *indexPtr++ = 5 + indexAdd;
*indexPtr++ = 2 + indexAdd; *indexPtr++ = 6 + indexAdd;
*indexPtr++ = 3 + indexAdd; *indexPtr++ = 7 + indexAdd;
*indexPtr++ = 4; *indexPtr++ = 5;
*indexPtr++ = 5; *indexPtr++ = 6;
*indexPtr++ = 6; *indexPtr++ = 7;
*indexPtr++ = 7; *indexPtr++ = 4;
*indexPtr++ = 4 + indexAdd; *indexPtr++ = 5 + indexAdd;
*indexPtr++ = 5 + indexAdd; *indexPtr++ = 6 + indexAdd;
*indexPtr++ = 6 + indexAdd; *indexPtr++ = 7 + indexAdd;
*indexPtr++ = 7 + indexAdd; *indexPtr++ = 4 + indexAdd;
}
}

View File

@@ -40,6 +40,7 @@ class SelectionBoxGeometry : public QQuick3DGeometry
Q_PROPERTY(QQuick3DNode *targetNode READ targetNode WRITE setTargetNode NOTIFY targetNodeChanged)
Q_PROPERTY(QQuick3DNode *rootNode READ rootNode WRITE setRootNode NOTIFY rootNodeChanged)
Q_PROPERTY(QQuick3DViewport *view3D READ view3D WRITE setView3D NOTIFY view3DChanged)
Q_PROPERTY(bool isEmpty READ isEmpty NOTIFY isEmptyChanged)
public:
SelectionBoxGeometry();
@@ -48,6 +49,7 @@ public:
QQuick3DNode *targetNode() const;
QQuick3DNode *rootNode() const;
QQuick3DViewport *view3D() const;
bool isEmpty() const;
public Q_SLOTS:
void setTargetNode(QQuick3DNode *targetNode);
@@ -58,17 +60,22 @@ Q_SIGNALS:
void targetNodeChanged();
void rootNodeChanged();
void view3DChanged();
void isEmptyChanged();
protected:
QSSGRenderGraphObject *updateSpatialNode(QSSGRenderGraphObject *node) override;
private:
void fillVertexData(QByteArray &vertexData, QByteArray &indexData,
const QVector3D &minBounds, const QVector3D &maxBounds);
void getBounds(QQuick3DNode *node, QByteArray &vertexData, QByteArray &indexData,
QVector3D &minBounds, QVector3D &maxBounds, const QMatrix4x4 &transform);
void appendVertexData(QByteArray &vertexData, QByteArray &indexData,
const QVector3D &minBounds, const QVector3D &maxBounds);
QQuick3DNode *m_targetNode = nullptr;
QQuick3DViewport *m_view3D = nullptr;
QQuick3DNode *m_rootNode = nullptr;
bool m_isEmpty = true;
QVector<QMetaObject::Connection> m_connections;
};
}

View File

@@ -340,12 +340,24 @@ QObject *Qt5InformationNodeInstanceServer::findRootNodeOf3DViewport(
{
for (const ServerNodeInstance &instance : instanceList) {
if (instance.isSubclassOf("QQuick3DViewport")) {
QObject *rootObj = nullptr;
int viewChildCount = 0;
for (const ServerNodeInstance &child : instanceList) { /* Look for scene node */
/* The QQuick3DViewport always creates a root node.
* This root node contains the complete scene. */
if (child.isSubclassOf("QQuick3DNode") && child.parent() == instance)
return child.internalObject()->property("parent").value<QObject *>();
if (child.isSubclassOf("QQuick3DNode") && child.parent() == instance) {
// Implicit root node is not visible in editor, so there is often another node
// added below it that serves as the actual scene root node.
// If the found root is the only node child of the view, assume that is the case.
++viewChildCount;
if (!rootObj)
rootObj = child.internalObject();
}
}
if (viewChildCount == 1)
return rootObj;
else if (rootObj)
return rootObj->property("parent").value<QObject *>();
}
}
return nullptr;
@@ -603,10 +615,8 @@ void Qt5InformationNodeInstanceServer::changeSelection(const ChangeSelectionComm
if (hasInstanceForId(id)) {
ServerNodeInstance instance = instanceForId(id);
QObject *object = nullptr;
if (instance.isSubclassOf("QQuick3DModel") || instance.isSubclassOf("QQuick3DCamera")
|| instance.isSubclassOf("QQuick3DAbstractLight")) {
if (instance.isSubclassOf("QQuick3DNode"))
object = instance.internalObject();
}
QMetaObject::invokeMethod(m_editView3D, "selectObject", Q_ARG(QVariant,
objectToVariant(object)));
return; // TODO: support multi-selection