// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "generalhelper.h" #ifdef QUICK3D_MODULE #include "selectionboxgeometry.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef QUICK3D_PARTICLES_MODULE #include #include #include #include #endif #include namespace QmlDesigner { namespace Internal { const QString _globalStateId = QStringLiteral("@GTS"); // global tool state const QString _lastSceneIdKey = QStringLiteral("lastSceneId"); const QString _rootSizeKey = QStringLiteral("rootSize"); static const float floatMin = std::numeric_limits::lowest(); static const float floatMax = std::numeric_limits::max(); static const QVector3D maxVec = QVector3D(floatMax, floatMax, floatMax); static const QVector3D minVec = QVector3D(floatMin, floatMin, floatMin); GeneralHelper::GeneralHelper() : QObject() { m_overlayUpdateTimer.setInterval(16); m_overlayUpdateTimer.setSingleShot(true); QObject::connect(&m_overlayUpdateTimer, &QTimer::timeout, this, &GeneralHelper::overlayUpdateNeeded); m_toolStateUpdateTimer.setSingleShot(true); QObject::connect(&m_toolStateUpdateTimer, &QTimer::timeout, this, &GeneralHelper::handlePendingToolStateUpdate); } void GeneralHelper::requestOverlayUpdate() { // Restart the timer on each request in attempt to ensure there's one frame between the last // request and actual update. m_overlayUpdateTimer.start(); } QString GeneralHelper::generateUniqueName(const QString &nameRoot) { static QHash counters; int count = counters[nameRoot]++; return QStringLiteral("%1_%2").arg(nameRoot).arg(count); } // Resolves absolute model source path QUrl GeneralHelper::resolveAbsoluteSourceUrl(const QQuick3DModel *sourceModel) { if (!sourceModel) return {}; const QUrl source = sourceModel->source(); if (source.hasFragment()) { // Fragment is part of the url separated by '#', check if it is an index or primitive bool isNumber = false; source.fragment().toInt(&isNumber); // If it wasn't an index, then it was a primitive and we can return it as-is if (!isNumber) return source; } QQmlContext *context = qmlContext(sourceModel); return context ? context->resolvedUrl(source) : source; } void GeneralHelper::orbitCamera(QQuick3DCamera *camera, const QVector3D &startRotation, const QVector3D &lookAtPoint, const QVector3D &pressPos, const QVector3D ¤tPos) { QVector3D dragVector = currentPos - pressPos; if (dragVector.length() < 0.001f) return; camera->setEulerRotation(startRotation); QVector3D newRotation(-dragVector.y(), -dragVector.x(), 0.f); newRotation *= 0.5f; // Emprically determined multiplier for nice drag newRotation += startRotation; camera->setEulerRotation(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 ¤tPos, 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([[maybe_unused]] QQuick3DViewport *viewPort, 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(.01f, zoomFactor * multiplier, 100.f) : zoomFactor; if (qobject_cast(camera)) { // Ortho camera we can simply scale float orthoFactor = newZoomFactor; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (viewPort) { if (const QQuickWindow *w = viewPort->window()) orthoFactor *= w->devicePixelRatio(); } #endif camera->setScale(QVector3D(orthoFactor, orthoFactor, orthoFactor)); } else if (qobject_cast(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; } // Return value contains new lookAt point (xyz) and zoom factor (w) QVector4D GeneralHelper::focusNodesToCamera(QQuick3DCamera *camera, float defaultLookAtDistance, const QVariant &nodes, QQuick3DViewport *viewPort, float oldZoom, bool updateZoom, bool closeUp) { if (!camera) return QVector4D(0.f, 0.f, 0.f, 1.f); QList nodeList; const QVariantList varNodes = nodes.value(); for (const auto &varNode : varNodes) { auto model = varNode.value(); if (model) nodeList.append(model); } // Get bounds QVector3D totalMinBound; QVector3D totalMaxBound; const qreal defaultExtent = 200.; if (!nodeList.isEmpty()) { static const float floatMin = std::numeric_limits::lowest(); static const float floatMax = std::numeric_limits::max(); totalMinBound = {floatMax, floatMax, floatMax}; totalMaxBound = {floatMin, floatMin, floatMin}; } else { const float halfExtent = defaultExtent / 2.f; totalMinBound = {-halfExtent, -halfExtent, -halfExtent}; totalMaxBound = {halfExtent, halfExtent, halfExtent}; } for (const auto node : std::as_const(nodeList)) { auto model = qobject_cast(node); qreal maxExtent = defaultExtent; QVector3D center = node->scenePosition(); if (model) { auto targetPriv = QQuick3DObjectPrivate::get(model); if (auto renderModel = static_cast(targetPriv->spatialNode)) { QWindow *window = static_cast(viewPort->window()); if (window) { #if QT_VERSION < QT_VERSION_CHECK(6, 5, 1) QSSGRef context; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) context = QSSGRenderContextInterface::getRenderContextInterface(quintptr(window)); #else context = targetPriv->sceneManager->rci; #endif if (!context.isNull()) { #else const auto &sm = targetPriv->sceneManager; auto context = sm->wattached ? sm->wattached->rci().get() : nullptr; if (context) { #endif QSSGBounds3 bounds; auto geometry = qobject_cast(model->geometry()); if (geometry) { bounds = geometry->bounds(); } else { const auto &bufferManager(context->bufferManager()); #if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) bounds = renderModel->getModelBounds(bufferManager); #else bounds = bufferManager->getModelBounds(renderModel); #endif } center = renderModel->globalTransform.map(bounds.center()); const QVector3D e = bounds.extents(); const QVector3D s = model->sceneScale(); qreal maxScale = qSqrt(qreal(s.x() * s.x() + s.y() * s.y() + s.z() * s.z())); maxExtent = qSqrt(qreal(e.x() * e.x() + e.y() * e.y() + e.z() * e.z())); maxExtent *= maxScale; if (maxExtent < 0.0001) maxExtent = defaultExtent; } } } } float halfExtent = float(maxExtent / 2.); const QVector3D halfExtents {halfExtent, halfExtent, halfExtent}; const QVector3D minBound = center - halfExtents; const QVector3D maxBound = center + halfExtents; for (int i = 0; i < 3; ++i) { totalMinBound[i] = qMin(minBound[i], totalMinBound[i]); totalMaxBound[i] = qMax(maxBound[i], totalMaxBound[i]); } } QVector3D extents = totalMaxBound - totalMinBound; QVector3D lookAt = totalMinBound + (extents / 2.f); float maxExtent = qMax(extents.x(), qMax(extents.y(), extents.z())); // Reset camera position to default zoom QMatrix4x4 m = camera->sceneTransform(); const float *dataPtr(m.data()); QVector3D newLookVector(dataPtr[8], dataPtr[9], dataPtr[10]); newLookVector.normalize(); newLookVector *= defaultLookAtDistance; camera->setPosition(lookAt + newLookVector); float divisor = closeUp ? 900.f : 725.f; float newZoomFactor = updateZoom ? qBound(.01f, maxExtent / divisor, 100.f) : oldZoom; float cameraZoomFactor = zoomCamera(viewPort, camera, 0, defaultLookAtDistance, lookAt, newZoomFactor, false); return QVector4D(lookAt, cameraZoomFactor); } // This function can be used to synchronously focus camera on a node, which doesn't have to be // a selection box for bound calculations to work. This is used to focus the view for // various preview image generations, where doing things asynchronously is not good // and recalculating bounds for every frame is not a problem. void GeneralHelper::calculateNodeBoundsAndFocusCamera( QQuick3DCamera *camera, QQuick3DNode *node, QQuick3DViewport *viewPort, float defaultLookAtDistance, bool closeUp) { QVector3D minBounds; QVector3D maxBounds; getBounds(viewPort, node, minBounds, maxBounds); QVector3D extents = maxBounds - minBounds; QVector3D lookAt = minBounds + (extents / 2.f); float maxExtent = qSqrt(qreal(extents.x()) * qreal(extents.x()) + qreal(extents.y()) * qreal(extents.y()) + qreal(extents.z()) * qreal(extents.z())); // Reset camera position to default zoom QMatrix4x4 m = camera->sceneTransform(); const float *dataPtr(m.data()); QVector3D newLookVector(dataPtr[8], dataPtr[9], dataPtr[10]); newLookVector.normalize(); newLookVector *= defaultLookAtDistance; camera->setPosition(lookAt + newLookVector); // CloseUp divisor is used for icon generation, where we can allow some extreme models to go // slightly out of bounds for better results generally. The other divisor is used for other // previews, where the image is larger to begin with and we would also like some margin // between preview edge and the rendered model, so we can be more conservative with the zoom. // The divisor values are empirically selected to provide nice result. float divisor = closeUp ? 1250.f : 1050.f; float newZoomFactor = maxExtent / divisor; zoomCamera(viewPort, camera, 0, defaultLookAtDistance, lookAt, newZoomFactor, false); if (auto perspectiveCamera = qobject_cast(camera)) { // Fix camera near/far clips in case we are dealing with extreme zooms const float cameraDist = qAbs((camera->position() - lookAt).length()); const float minDist = cameraDist - (maxExtent / 2.f); const float maxDist = cameraDist + (maxExtent / 2.f); if (minDist < perspectiveCamera->clipNear() || maxDist > perspectiveCamera->clipFar()) { perspectiveCamera->setClipNear(minDist * 0.99); perspectiveCamera->setClipFar(maxDist * 1.01); } } } // Aligns any cameras found in nodes list to a camera. // Only position and rotation are copied, rest of the camera properties stay the same. void GeneralHelper::alignCameras(QQuick3DCamera *camera, const QVariant &nodes) { QList nodeList; const QVariantList varNodes = nodes.value(); for (const auto &varNode : varNodes) { auto cameraNode = varNode.value(); if (cameraNode) nodeList.append(cameraNode); } for (QQuick3DCamera *node : std::as_const(nodeList)) { node->setPosition(camera->position()); node->setRotation(camera->rotation()); } } // Aligns the camera to the first camera in nodes list. // Aligning means taking the position and XY rotation from the source camera. Rest of the properties // remain the same, as this is used to align edit cameras, which have fixed Z-rot, fov, and clips. // The new lookAt is set at same distance away as it was previously and scale isn't adjusted, so // the zoom factor of the edit camera stays the same. QVector3D GeneralHelper::alignView(QQuick3DCamera *camera, const QVariant &nodes, const QVector3D &lookAtPoint) { float lastDistance = (lookAtPoint - camera->position()).length(); const QVariantList varNodes = nodes.value(); QQuick3DCamera *cameraNode = nullptr; for (const auto &varNode : varNodes) { cameraNode = varNode.value(); if (cameraNode) break; } if (cameraNode) { camera->setPosition(cameraNode->position()); QVector3D newRotation = cameraNode->eulerRotation(); newRotation.setZ(0.f); camera->setEulerRotation(newRotation); } QVector3D lookAt = camera->position() + camera->forward() * lastDistance; return lookAt; } bool GeneralHelper::fuzzyCompare(double a, double b) { return qFuzzyCompare(a, b); } void GeneralHelper::delayedPropertySet(QObject *obj, int delay, const QString &property, const QVariant &value) { QTimer::singleShot(delay, [obj, property, value]() { obj->setProperty(property.toLatin1().constData(), value); }); } // Returns the first valid QQuick3DPickResult from view at (posX, PosY). QQuick3DPickResult GeneralHelper::pickViewAt(QQuick3DViewport *view, float posX, float posY) { if (!view) return QQuick3DPickResult(); #if QT_VERSION >= QT_VERSION_CHECK(6, 2, 1) // Make sure global picking is on view->setGlobalPickingEnabled(true); // With Qt 6.2+, select first suitable result from all picked objects auto pickResults = view->pickAll(posX, posY); for (auto pickResult : pickResults) { if (isPickable(pickResult.objectHit())) return pickResult; } #else // With older Qt version we'll just pick the single object auto pickResult = view->pick(posX, posY); if (isPickable(pickResult.objectHit())) return pickResult; #endif return QQuick3DPickResult(); } QObject *GeneralHelper::resolvePick(QQuick3DNode *pickNode) { if (pickNode) { // Check if the picked node actually specifies another object as the pick target QVariant componentVar = pickNode->property("_pickTarget"); if (componentVar.isValid()) { auto componentObj = componentVar.value(); if (componentObj) return componentObj; } } return pickNode; } bool GeneralHelper::isLocked(QQuick3DNode *node) const { if (node) { QVariant lockValue = node->property("_edit3dLocked"); return lockValue.isValid() && lockValue.toBool(); } return false; } bool GeneralHelper::isHidden(QQuick3DNode *node) const { if (node) { QVariant hideValue = node->property("_edit3dHidden"); return hideValue.isValid() && hideValue.toBool(); } return false; } bool GeneralHelper::isPickable(QQuick3DNode *node) const { if (!node) return false; #if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) // Instancing doesn't hide child nodes, so only check for instancing on the requested node if (auto model = qobject_cast(node)) { if (model->instancing()) return false; } #endif QQuick3DNode *n = node; while (n) { if (!n->visible() || isLocked(n) || isHidden(n)) return false; n = n->parentNode(); } return true; } // Emitter gizmo model creation is done in C++ as creating dynamic properties and // assigning materials to dynamically created models is lot simpler in C++ QQuick3DNode *GeneralHelper::createParticleEmitterGizmoModel(QQuick3DNode *emitter, QQuick3DMaterial *material) const { #ifdef QUICK3D_PARTICLES_MODULE if (qobject_cast(emitter) || !material) return nullptr; QQuick3DParticleModelShape *shape = nullptr; if (auto e = qobject_cast(emitter)) shape = qobject_cast(e->shape()); else if (auto a = qobject_cast(emitter)) shape = qobject_cast(a->shape()); if (shape && shape->delegate()) { if (auto model = qobject_cast( shape->delegate()->create(shape->delegate()->creationContext()))) { QQmlEngine::setObjectOwnership(model, QQmlEngine::JavaScriptOwnership); model->setProperty("_pickTarget", QVariant::fromValue(emitter)); QQmlListReference matRef(model, "materials"); matRef.append(material); return model; } } #endif return nullptr; } void GeneralHelper::storeToolState(const QString &sceneId, const QString &tool, const QVariant &state, int delay) { if (delay > 0) { QVariantMap sceneToolState; sceneToolState.insert(tool, state); m_toolStatesPending.insert(sceneId, sceneToolState); m_toolStateUpdateTimer.start(delay); } else { if (m_toolStateUpdateTimer.isActive()) handlePendingToolStateUpdate(); QVariant theState; // Convert JS arrays to QVariantLists for easier handling down the line // metaType().id() which only exist in Qt6 is the same as userType() if (state.userType() != QMetaType::QString && state.canConvert(QMetaType::QVariantList)) theState = state.value(); else theState = state; QVariantMap &sceneToolState = m_toolStates[sceneId]; if (sceneToolState[tool] != theState) { sceneToolState.insert(tool, theState); emit toolStateChanged(sceneId, tool, theState); } } } void GeneralHelper::setSceneEnvironmentColor(const QString &sceneId, const QColor &color) { m_sceneEnvironmentColor[sceneId] = color; } QColor GeneralHelper::sceneEnvironmentColor(const QString &sceneId) const { return m_sceneEnvironmentColor[sceneId]; } void GeneralHelper::initToolStates(const QString &sceneId, const QVariantMap &toolStates) { m_toolStates[sceneId] = toolStates; } void GeneralHelper::enableItemUpdate(QQuickItem *item, bool enable) { if (item) item->setFlag(QQuickItem::ItemHasContents, enable); } QVariantMap GeneralHelper::getToolStates(const QString &sceneId) { handlePendingToolStateUpdate(); if (m_toolStates.contains(sceneId)) return m_toolStates[sceneId]; return {}; } QString GeneralHelper::globalStateId() const { return _globalStateId; } QString GeneralHelper::lastSceneIdKey() const { return _lastSceneIdKey; } QString GeneralHelper::rootSizeKey() const { return _rootSizeKey; } double GeneralHelper::brightnessScaler() const { // Light brightness was rescaled in Qt6 from 100 -> 1. #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) return 100.; #else return 1.; #endif } void GeneralHelper::setMultiSelectionTargets(QQuick3DNode *multiSelectRootNode, const QVariantList &selectedList) { // Filter selection to contain only topmost parent nodes in the selection m_multiSelDataMap.clear(); m_multiSelNodes.clear(); for (auto &connection : std::as_const(m_multiSelectConnections)) disconnect(connection); m_multiSelectConnections.clear(); m_multiSelectRootNode = multiSelectRootNode; QSet selNodes; for (const auto &var : selectedList) { QQuick3DNode *node = nullptr; node = var.value(); if (node) selNodes.insert(node); } for (const auto selNode : std::as_const(selNodes)) { bool found = false; QQuick3DNode *parent = selNode->parentNode(); while (parent) { if (selNodes.contains(parent)) { found = true; break; } parent = parent->parentNode(); } if (!found) { m_multiSelDataMap.insert(selNode, {}); m_multiSelNodes.append(QVariant::fromValue(selNode)); m_multiSelectConnections.append(connect(selNode, &QObject::destroyed, [this]() { // If any multiselected node is destroyed, assume the entire selection is invalid. // The new selection should be notified by creator immediately after anyway. m_multiSelDataMap.clear(); m_multiSelNodes.clear(); for (auto &connection : std::as_const(m_multiSelectConnections)) disconnect(connection); m_multiSelectConnections.clear(); })); m_multiSelectConnections.append(connect(selNode, &QQuick3DNode::sceneTransformChanged, [this]() { // Reposition the multiselection root node if scene transform of any multiselected // node changes outside of drag (i.e. changes originating from creator side) if (!m_blockMultiSelectionNodePositioning) resetMultiSelectionNode(); })); } } resetMultiSelectionNode(); m_blockMultiSelectionNodePositioning = false; } void GeneralHelper::resetMultiSelectionNode() { for (auto it = m_multiSelDataMap.begin(); it != m_multiSelDataMap.end(); ++it) it.value() = {pivotScenePosition(it.key()), it.key()->scale(), it.key()->rotation(), it.key()->sceneRotation()}; m_multiSelNodeData = {}; if (!m_multiSelDataMap.isEmpty()) { for (const auto &data : std::as_const(m_multiSelDataMap)) m_multiSelNodeData.startScenePos += data.startScenePos; m_multiSelNodeData.startScenePos /= m_multiSelDataMap.size(); } m_multiSelectRootNode->setPosition(m_multiSelNodeData.startScenePos); m_multiSelectRootNode->setRotation({}); m_multiSelectRootNode->setScale({1.f, 1.f, 1.f}); } void GeneralHelper::restartMultiSelection() { resetMultiSelectionNode(); m_blockMultiSelectionNodePositioning = true; } QVariantList GeneralHelper::multiSelectionTargets() const { return m_multiSelNodes; } void GeneralHelper::moveMultiSelection(bool commit) { // Move the multiselected nodes in global space by offset from multiselection start to scenePos QVector3D globalOffset = m_multiSelectRootNode->scenePosition() - m_multiSelNodeData.startScenePos; for (auto it = m_multiSelDataMap.constBegin(); it != m_multiSelDataMap.constEnd(); ++it) { QVector3D newGlobalPos = it.value().startScenePos + globalOffset; QMatrix4x4 m; if (it.key()->parentNode()) m = it.key()->parentNode()->sceneTransform(); it.key()->setPosition(m.inverted() * newGlobalPos); } m_blockMultiSelectionNodePositioning = !commit; } void GeneralHelper::scaleMultiSelection(bool commit) { // Offset the multiselected nodes in global space according to scale factor and scale them by // the same factor. const QVector3D sceneScale = m_multiSelectRootNode->scale(); const QVector3D unitVector {1.f, 1.f, 1.f}; const QVector3D diffScale = sceneScale - unitVector; for (auto it = m_multiSelDataMap.constBegin(); it != m_multiSelDataMap.constEnd(); ++it) { const QVector3D newGlobalPos = m_multiSelNodeData.startScenePos + (it.value().startScenePos - m_multiSelNodeData.startScenePos) * sceneScale; QMatrix4x4 parentMat; if (it.key()->parentNode()) parentMat = it.key()->parentNode()->sceneTransform().inverted(); it.key()->setPosition(parentMat * newGlobalPos); QMatrix4x4 mat; mat.rotate(it.value().startSceneRot); auto scaleDim = [&](int dim) -> QVector3D { QVector3D dimScale; float diffScaleDim = diffScale[dim]; dimScale[dim] = diffScaleDim; dimScale = (mat.inverted() * dimScale).normalized() * diffScaleDim; for (int i = 0; i < 3; ++i) dimScale[i] = qAbs(dimScale[i]); if (sceneScale[dim] < 1.0f) dimScale = -dimScale; return dimScale; }; QVector3D finalScale = scaleDim(0) + scaleDim(1) + scaleDim(2) + unitVector; it.key()->setScale(finalScale * it.value().startScale); } m_blockMultiSelectionNodePositioning = !commit; } void GeneralHelper::rotateMultiSelection(bool commit) { // Rotate entire selection around the multiselection node const QQuaternion sceneRotation = m_multiSelectRootNode->sceneRotation(); QVector3D rotAxis; float rotAngle = 0; sceneRotation.getAxisAndAngle(&rotAxis, &rotAngle); for (auto it = m_multiSelDataMap.constBegin(); it != m_multiSelDataMap.constEnd(); ++it) { QVector3D globalOffset = it.value().startScenePos - m_multiSelNodeData.startScenePos; QVector3D newGlobalPos = m_multiSelNodeData.startScenePos + sceneRotation * globalOffset; QMatrix4x4 parentMat; if (it.key()->parentNode()) parentMat = it.key()->parentNode()->sceneTransform().inverted(); it.key()->setPosition(parentMat * newGlobalPos); it.key()->setRotation(it.value().startRot); it.key()->rotate(rotAngle, rotAxis, QQuick3DNode::SceneSpace); } m_blockMultiSelectionNodePositioning = !commit; } bool GeneralHelper::isMacOS() const { #ifdef Q_OS_MACOS return true; #else return false; #endif } void GeneralHelper::addRotationBlocks(const QSet &nodes) { m_rotationBlockedNodes.unite(nodes); emit rotationBlocksChanged(); } void GeneralHelper::removeRotationBlocks(const QSet &nodes) { for (auto node : nodes) m_rotationBlockedNodes.remove(node); emit rotationBlocksChanged(); } bool GeneralHelper::isRotationBlocked(QQuick3DNode *node) const { return m_rotationBlockedNodes.contains(node); } void GeneralHelper::handlePendingToolStateUpdate() { m_toolStateUpdateTimer.stop(); auto sceneIt = m_toolStatesPending.constBegin(); while (sceneIt != m_toolStatesPending.constEnd()) { const QVariantMap &sceneToolState = sceneIt.value(); auto toolIt = sceneToolState.constBegin(); while (toolIt != sceneToolState.constEnd()) { storeToolState(sceneIt.key(), toolIt.key(), toolIt.value()); ++toolIt; } ++sceneIt; } m_toolStatesPending.clear(); } // Calculate scene position of the node's pivot point, which in practice is just the position // of the node without applying the pivot offset. QVector3D GeneralHelper::pivotScenePosition(QQuick3DNode *node) const { if (!node) return {}; QQuick3DNode *parent = node->parentNode(); if (!parent) return node->position(); QMatrix4x4 localTransform; localTransform.translate(node->position()); const QMatrix4x4 sceneTransform = parent->sceneTransform() * localTransform; return mat44::getPosition(sceneTransform); } // Calculate bounds for given node, including all child nodes. // Returns true if the tree contains at least one Model node. bool GeneralHelper::getBounds(QQuick3DViewport *view3D, QQuick3DNode *node, QVector3D &minBounds, QVector3D &maxBounds) { if (!node) { const float halfExtent = 100.f; minBounds = {-halfExtent, -halfExtent, -halfExtent}; maxBounds = {halfExtent, halfExtent, halfExtent}; return false; } QMatrix4x4 localTransform; auto nodePriv = QQuick3DObjectPrivate::get(node); auto renderNode = static_cast(nodePriv->spatialNode); if (renderNode) { #if QT_VERSION < QT_VERSION_CHECK(6, 4, 0) if (renderNode->flags.testFlag(QSSGRenderNode::Flag::TransformDirty)) renderNode->calculateLocalTransform(); #else if (renderNode->isDirty(QSSGRenderNode::DirtyFlag::TransformDirty)) { renderNode->localTransform = QSSGRenderNode::calculateTransformMatrix( node->position(), node->scale(), node->pivot(), node->rotation()); } #endif localTransform = renderNode->localTransform; } QVector3D localMinBounds = maxVec; QVector3D localMaxBounds = minVec; // Find bounds for children QVector minBoundsVec; QVector maxBoundsVec; const auto children = node->childItems(); bool hasModel = false; for (const auto child : children) { if (auto childNode = qobject_cast(child)) { QVector3D newMinBounds = minBounds; QVector3D newMaxBounds = maxBounds; bool childHasModel = getBounds(view3D, childNode, newMinBounds, newMaxBounds); // Ignore any subtrees that do not have Model in them as we don't need those // for visual bounds calculations if (childHasModel) { minBoundsVec << newMinBounds; maxBoundsVec << newMaxBounds; hasModel = true; } } } auto combineMinBounds = [](QVector3D &target, const QVector3D &source) { target.setX(qMin(source.x(), target.x())); target.setY(qMin(source.y(), target.y())); target.setZ(qMin(source.z(), target.z())); }; auto combineMaxBounds = [](QVector3D &target, const QVector3D &source) { target.setX(qMax(source.x(), target.x())); target.setY(qMax(source.y(), target.y())); target.setZ(qMax(source.z(), target.z())); }; auto transformCorner = [&](const QMatrix4x4 &m, QVector3D &minTarget, QVector3D &maxTarget, const QVector3D &corner) { QVector3D mappedCorner = m.map(corner); combineMinBounds(minTarget, mappedCorner); combineMaxBounds(maxTarget, mappedCorner); }; auto transformCorners = [&](const QMatrix4x4 &m, QVector3D &minTarget, QVector3D &maxTarget, const QVector3D &minCorner, const QVector3D &maxCorner) { transformCorner(m, minTarget, maxTarget, minCorner); transformCorner(m, minTarget, maxTarget, maxCorner); transformCorner(m, minTarget, maxTarget, QVector3D(minCorner.x(), minCorner.y(), maxCorner.z())); transformCorner(m, minTarget, maxTarget, QVector3D(minCorner.x(), maxCorner.y(), minCorner.z())); transformCorner(m, minTarget, maxTarget, QVector3D(maxCorner.x(), minCorner.y(), minCorner.z())); transformCorner(m, minTarget, maxTarget, QVector3D(minCorner.x(), maxCorner.y(), maxCorner.z())); transformCorner(m, minTarget, maxTarget, QVector3D(maxCorner.x(), maxCorner.y(), minCorner.z())); transformCorner(m, minTarget, maxTarget, QVector3D(maxCorner.x(), minCorner.y(), maxCorner.z())); }; // Combine all child bounds for (const auto &newBounds : std::as_const(minBoundsVec)) combineMinBounds(localMinBounds, newBounds); for (const auto &newBounds : std::as_const(maxBoundsVec)) combineMaxBounds(localMaxBounds, newBounds); if (qobject_cast(node)) { if (auto renderModel = static_cast(renderNode)) { QWindow *window = static_cast(view3D->window()); if (window) { #if QT_VERSION < QT_VERSION_CHECK(6, 5, 1) QSSGRef context; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) context = QSSGRenderContextInterface::getRenderContextInterface(quintptr(window)); #else context = QQuick3DObjectPrivate::get(node)->sceneManager->rci; #endif if (!context.isNull()) { #else const auto &sm = QQuick3DObjectPrivate::get(node)->sceneManager; auto context = sm->wattached ? sm->wattached->rci().get() : nullptr; if (context) { #endif const auto &bufferManager(context->bufferManager()); #if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) QSSGBounds3 bounds = renderModel->getModelBounds(bufferManager); #else QSSGBounds3 bounds = bufferManager->getModelBounds(renderModel); #endif QVector3D center = bounds.center(); QVector3D extents = bounds.extents(); QVector3D localMin = center - extents; QVector3D localMax = center + extents; combineMinBounds(localMinBounds, localMin); combineMaxBounds(localMaxBounds, localMax); hasModel = true; } } } } else { combineMinBounds(localMinBounds, {}); combineMaxBounds(localMaxBounds, {}); } if (localMaxBounds == minVec) { localMinBounds = {}; localMaxBounds = {}; } // Transform local space bounding box to parent space transformCorners(localTransform, minBounds, maxBounds, localMinBounds, localMaxBounds); return hasModel; } } } #endif // QUICK3D_MODULE