// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "q3dscene.h" #include "qgraphsinputhandler_p.h" #include "qquickgraphsscatter_p.h" #include "qquickgraphstexturedata_p.h" #include "qscatter3dseries_p.h" #include "qscatterdataproxy_p.h" #include "qvalue3daxis_p.h" #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE static const int insertRemoveRecordReserveSize = 31; /*! * \qmltype Scatter3D * \inherits GraphsItem3D * \inqmlmodule QtGraphs * \ingroup graphs_qml_3D * \brief 3D scatter graph. * * This type enables developers to render scatter graphs in 3D with Qt Quick. * * You will need to import Qt Graphs module to use this type: * * \snippet doc_src_qmlgraphs.cpp 0 * * After that you can use Scatter3D in your qml files: * * \snippet doc_src_qmlgraphs.cpp 2 * * See \l{Simple Scatter Graph} for more thorough usage example. * * \sa Scatter3DSeries, Spline3DSeries, ScatterDataProxy, Bars3D, Surface3D, * {Qt Graphs C++ Classes for 3D} */ /*! * \qmlproperty Value3DAxis Scatter3D::axisX * The active x-axis. * * If an axis is not given, a temporary default axis with no labels and an * automatically adjusting range is created. * This temporary axis is destroyed if another axis is explicitly set to the * same orientation. */ /*! * \qmlproperty Value3DAxis Scatter3D::axisY * The active y-axis. * * If an axis is not given, a temporary default axis with no labels and an * automatically adjusting range is created. * This temporary axis is destroyed if another axis is explicitly set to the * same orientation. */ /*! * \qmlproperty Value3DAxis Scatter3D::axisZ * The active z-axis. * * If an axis is not given, a temporary default axis with no labels and an * automatically adjusting range is created. * This temporary axis is destroyed if another axis is explicitly set to the * same orientation. */ /*! * \qmlproperty Scatter3DSeries Scatter3D::selectedSeries * \readonly * * The selected series or null. */ /*! * \qmlproperty list Scatter3D::seriesList * \qmldefault * This property holds the series of the graph. * By default, this property contains an empty list. * To set the series, either use the addSeries() method or define them as * children of the graph. */ /*! * \qmlmethod void Scatter3D::addSeries(Scatter3DSeries series) * Adds the \a series to the graph. A graph can contain multiple series, but has * only one set of axes. If the newly added series has specified a selected * item, it will be highlighted and any existing selection will be cleared. Only * one added series can have an active selection. * \sa GraphsItem3D::hasSeries() */ /*! * \qmlmethod void Scatter3D::removeSeries(Scatter3DSeries series) * Remove the \a series from the graph. * \sa GraphsItem3D::hasSeries() */ /*! * \qmlsignal Scatter3D::axisXChanged(ValueAxis3D axis) * * This signal is emitted when axisX changes to \a axis. */ /*! * \qmlsignal Scatter3D::axisYChanged(ValueAxis3D axis) * * This signal is emitted when axisY changes to \a axis. */ /*! * \qmlsignal Scatter3D::axisZChanged(ValueAxis3D axis) * * This signal is emitted when axisZ changes to \a axis. */ /*! * \qmlsignal Scatter3D::selectedSeriesChanged(Scatter3DSeries series) * * This signal is emitted when selectedSeries changes to \a series. */ QQuickGraphsScatter::QQuickGraphsScatter(QQuickItem *parent) : QQuickGraphsItem(parent) { m_graphType = QAbstract3DSeries::SeriesType::Scatter; setAxisX(0); setAxisY(0); setAxisZ(0); setAcceptedMouseButtons(Qt::AllButtons); setFlag(ItemHasContents); clearSelection(); } QQuickGraphsScatter::~QQuickGraphsScatter() { QMutexLocker locker(m_nodeMutex.data()); const QMutexLocker locker2(mutex()); for (auto &graphModel : m_scatterGraphs) { delete graphModel; } } void QQuickGraphsScatter::setAxisX(QValue3DAxis *axis) { QQuickGraphsItem::setAxisX(axis); } QValue3DAxis *QQuickGraphsScatter::axisX() const { return static_cast(QQuickGraphsItem::axisX()); } void QQuickGraphsScatter::setAxisY(QValue3DAxis *axis) { QQuickGraphsItem::setAxisY(axis); } QValue3DAxis *QQuickGraphsScatter::axisY() const { return static_cast(QQuickGraphsItem::axisY()); } void QQuickGraphsScatter::setAxisZ(QValue3DAxis *axis) { QQuickGraphsItem::setAxisZ(axis); } QValue3DAxis *QQuickGraphsScatter::axisZ() const { return static_cast(QQuickGraphsItem::axisZ()); } void QQuickGraphsScatter::disconnectSeries(QScatter3DSeries *series) { QObject::disconnect(series, 0, this, 0); } void QQuickGraphsScatter::generatePointsForScatterModel(ScatterModel *graphModel) { QList itemList; if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) { qsizetype itemCount = graphModel->series->dataProxy()->itemCount(); if (graphModel->series->dataProxy()->itemCount() > 0) itemList.resize(itemCount); for (int i = 0; i < itemCount; i++) { QQuick3DModel *item = createDataItem(graphModel->series); item->setPickable(true); item->setParent(graphModel->series); itemList[i] = item; } graphModel->dataItems = itemList; markDataDirty(); } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) { graphModel->instancingRootItem = createDataItem(graphModel->series); graphModel->instancingRootItem->setParent(graphModel->series); graphModel->instancingRootItem->setInstancing(graphModel->instancing); if (selectionMode() != QtGraphs3D::SelectionFlag::None) { graphModel->selectionIndicator = createDataItem(graphModel->series); graphModel->instancingRootItem->setPickable(true); } } markSeriesVisualsDirty(); } qsizetype QQuickGraphsScatter::getItemIndex(QQuick3DModel *item) { Q_UNUSED(item); if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) return 0; return -1; } void QQuickGraphsScatter::clearSelection() { clearSelectionModel(); } void QQuickGraphsScatter::updateScatterGraphItemPositions(ScatterModel *graphModel) { float itemSize = graphModel->series->itemSize() / m_itemScaler; QQuaternion meshRotation = graphModel->series->meshRotation(); QScatterDataProxy *dataProxy = graphModel->series->dataProxy(); QList itemList = graphModel->dataItems; auto valueAxisX = static_cast(axisX()); auto valueAxisY = static_cast(axisY()); auto valueAxisZ = static_cast(axisZ()); bool xReversed = valueAxisX->reversed(); bool yReversed = valueAxisY->reversed(); bool zReversed = valueAxisZ->reversed(); if (itemSize == 0.0f) itemSize = m_pointScale; if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) { if (dataProxy->itemCount() != itemList.size()) { qWarning("%ls Item count differs from itemList count", qUtf16Printable(QString::fromUtf8(__func__))); } for (int i = 0; i < dataProxy->itemCount(); ++i) { const QScatterDataItem item = dataProxy->itemAt(i); QQuick3DModel *dataPoint = itemList.at(i); QVector3D dotPos = item.position(); if (isDotPositionInAxisRange(dotPos)) { dataPoint->setVisible(true); QQuaternion dotRot = item.rotation(); float dotPosX = xReversed ? 1.0f - valueAxisX->positionAt(dotPos.x()) : valueAxisX->positionAt(dotPos.x()); float dotPosY = yReversed ? 1.0f - valueAxisY->positionAt(dotPos.y()) : valueAxisY->positionAt(dotPos.y()); float dotPosZ = zReversed ? 1.0f - valueAxisZ->positionAt(dotPos.z()) : valueAxisZ->positionAt(dotPos.z()); float posX = dotPosX * scale().x() + translate().x(); float posY = dotPosY * scale().y() + translate().y(); float posZ = dotPosZ * scale().z() + translate().z(); dataPoint->setPosition(QVector3D(posX, posY, posZ)); QQuaternion totalRotation; if (graphModel->series->mesh() != QAbstract3DSeries::Mesh::Point) totalRotation = dotRot * meshRotation; else totalRotation = cameraTarget()->rotation(); dataPoint->setRotation(totalRotation); dataPoint->setScale(QVector3D(itemSize, itemSize, itemSize)); } else { dataPoint->setVisible(false); } } } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) { qsizetype count = dataProxy->itemCount(); QList positions; for (int i = 0; i < count; i++) { const QScatterDataItem &item = dataProxy->itemAt(i); QVector3D dotPos = item.position(); if (isDotPositionInAxisRange(dotPos)) { float dotPosX = xReversed ? 1.0f - valueAxisX->positionAt(dotPos.x()) : valueAxisX->positionAt(dotPos.x()); float dotPosY = yReversed ? 1.0f - valueAxisY->positionAt(dotPos.y()) : valueAxisY->positionAt(dotPos.y()); float dotPosZ = zReversed ? 1.0f - valueAxisZ->positionAt(dotPos.z()) : valueAxisZ->positionAt(dotPos.z()); float posX = dotPosX * scale().x() + translate().x(); float posY = dotPosY * scale().y() + translate().y(); float posZ = dotPosZ * scale().z() + translate().z(); QQuaternion totalRotation; if (graphModel->series->mesh() != QAbstract3DSeries::Mesh::Point) totalRotation = item.rotation() * meshRotation; else totalRotation = cameraTarget()->rotation(); DataItemHolder dih; if (isPolar()) { float x; float z; calculatePolarXZ(axisX()->positionAt(dotPos.x()), axisZ()->positionAt(dotPos.z()), x, z); dih.position = {x, posY, z}; } else { dih.position = {posX, posY, posZ}; } dih.rotation = totalRotation; dih.scale = {itemSize, itemSize, itemSize}; positions.push_back(dih); } else { DataItemHolder dih; dih.hide = true; positions.push_back(dih); } } graphModel->instancing->setDataArray(positions); if (selectedItemInSeries(graphModel->series)) { if (isDotPositionInAxisRange(dataProxy->itemAt(m_selectedItem).position())) { QQuaternion totalRotation; if (graphModel->series->mesh() != QAbstract3DSeries::Mesh::Point) { totalRotation = graphModel->instancing->dataArray().at(m_selectedItem).rotation * meshRotation; } else { totalRotation = cameraTarget()->rotation(); } graphModel->selectionIndicator->setRotation(totalRotation); graphModel->instancing->hideDataItem(m_selectedItem); } else { clearSelectionModel(); } } } } void QQuickGraphsScatter::updateScatterGraphItemVisuals(ScatterModel *graphModel) { bool useGradient = graphModel->series->d_func()->isUsingGradient(); bool usePoint = graphModel->series->mesh() == QAbstract3DSeries::Mesh::Point; qsizetype itemCount = graphModel->series->dataProxy()->itemCount(); if (useGradient) { if (!graphModel->seriesTexture) { graphModel->seriesTexture = createTexture(); graphModel->seriesTexture->setParent(graphModel->series); } QLinearGradient gradient = graphModel->series->baseGradient(); auto textureData = static_cast( graphModel->seriesTexture->textureData()); textureData->createGradient(gradient); if (!graphModel->highlightTexture) { graphModel->highlightTexture = createTexture(); graphModel->highlightTexture->setParent(graphModel->series); } QLinearGradient highlightGradient = graphModel->series->singleHighlightGradient(); auto highlightTextureData = static_cast( graphModel->highlightTexture->textureData()); highlightTextureData->createGradient(highlightGradient); } else { if (graphModel->seriesTexture) { graphModel->seriesTexture->deleteLater(); graphModel->seriesTexture = nullptr; } if (graphModel->highlightTexture) { graphModel->highlightTexture->deleteLater(); graphModel->highlightTexture = nullptr; } } bool rangeGradient = (useGradient && graphModel->series->d_func()->m_colorStyle == QGraphsTheme::ColorStyle::RangeGradient) ? true : false; if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) { // Release resources that might not have been deleted even though deleteLater had been set if (m_customView) m_customView->window()->releaseResources(); else window()->releaseResources(); if (itemCount != graphModel->dataItems.size()) qWarning("%ls Item count differs from itemList count", qUtf16Printable(QString::fromUtf8(__func__))); bool transparentTexture = false; if (graphModel->seriesTexture) { auto textureData = static_cast( graphModel->seriesTexture->textureData()); transparentTexture = textureData->hasTransparency(); } const bool transparency = (graphModel->series->baseColor().alphaF() < 1.0) || transparentTexture; updateMaterialReference(graphModel); QQmlListReference baseRef(graphModel->baseRef, "materials"); auto baseMat = static_cast(baseRef.at(0)); for (const auto &obj : std::as_const(graphModel->dataItems)) { QQmlListReference matRef(obj, "materials"); matRef.clear(); matRef.append(baseMat); } if (m_selectedItem != invalidSelectionIndex() && graphModel->series == selectedSeries()) { QQmlListReference selRef(graphModel->selectionRef, "materials"); auto selMat = static_cast(selRef.at(0)); QQuick3DModel *selectedItem = graphModel->dataItems.at(m_selectedItem); QQmlListReference matRef(selectedItem, "materials"); matRef.clear(); matRef.append(selMat); } updateItemMaterial(graphModel->baseRef, useGradient, rangeGradient, usePoint, QStringLiteral(":/materials/ScatterMaterial")); updateItemMaterial(graphModel->selectionRef, useGradient, rangeGradient, usePoint, QStringLiteral(":/materials/ScatterMaterial")); updateMaterialProperties(graphModel->baseRef, graphModel->seriesTexture, graphModel->series->baseColor(), transparency); updateMaterialProperties(graphModel->selectionRef, graphModel->highlightTexture, graphModel->series->singleHighlightColor()); } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) { graphModel->instancingRootItem->setVisible(true); graphModel->instancing->setRangeGradient(rangeGradient); if (!rangeGradient) { bool transparentTexture = false; if (graphModel->seriesTexture) { auto textureData = static_cast( graphModel->seriesTexture->textureData()); transparentTexture = textureData->hasTransparency(); } const bool transparency = (graphModel->series->baseColor().alphaF() < 1.0) || transparentTexture; if (transparency) graphModel->instancing->setTransparency(true); else graphModel->instancing->setTransparency(false); updateItemMaterial(graphModel->instancingRootItem, useGradient, rangeGradient, usePoint, QStringLiteral(":/materials/ScatterMaterialInstancing")); updateMaterialProperties(graphModel->instancingRootItem, graphModel->seriesTexture, graphModel->series->baseColor(), transparency); } else { auto textureData = static_cast( graphModel->seriesTexture->textureData()); if (textureData->hasTransparency()) graphModel->instancing->setTransparency(true); else graphModel->instancing->setTransparency(false); updateItemMaterial(graphModel->instancingRootItem, useGradient, rangeGradient, usePoint, QStringLiteral(":/materials/ScatterMaterialInstancing")); updateInstancedMaterialProperties(graphModel, false, graphModel->seriesTexture, graphModel->highlightTexture, textureData->hasTransparency()); const float scaleY = scaleWithBackground().y(); float rangeGradientYScaler = m_rangeGradientYHelper / scaleY; QList customData; customData.resize(itemCount); QList instancingData = graphModel->instancing->dataArray(); for (int i = 0; i < instancingData.size(); i++) { auto dih = instancingData.at(i); float value = (dih.position.y() + scaleY) * rangeGradientYScaler; customData[i] = value; } graphModel->instancing->setCustomData(customData); } if (selectedItemInSeries(graphModel->series)) { // Selection indicator if (!rangeGradient) { updateItemMaterial(graphModel->selectionIndicator, useGradient, rangeGradient, usePoint, QStringLiteral(":/materials/ScatterMaterial")); updateMaterialProperties(graphModel->selectionIndicator, graphModel->highlightTexture, graphModel->series->singleHighlightColor()); graphModel->selectionIndicator->setCastsShadows(!usePoint); } else { // Rangegradient updateItemMaterial(graphModel->selectionIndicator, useGradient, rangeGradient, usePoint, QStringLiteral(":/materials/ScatterMaterial")); updateInstancedMaterialProperties(graphModel, true, nullptr, graphModel->highlightTexture); graphModel->selectionIndicator->setCastsShadows(!usePoint); } const DataItemHolder &dih = graphModel->instancing->dataArray().at(m_selectedItem); graphModel->selectionIndicator->setPosition(dih.position); graphModel->selectionIndicator->setRotation(dih.rotation); graphModel->selectionIndicator->setScale(dih.scale); graphModel->selectionIndicator->setVisible(true); graphModel->instancing->hideDataItem(m_selectedItem); updateItemLabel(graphModel->selectionIndicator->position()); graphModel->instancing->markDataDirty(); } else if ((m_selectedItem == -1 || m_selectedItemSeries != graphModel->series) && graphModel->selectionIndicator) { graphModel->selectionIndicator->setVisible(false); } } } void QQuickGraphsScatter::updateMaterialReference(ScatterModel *model) { if (model->baseRef == nullptr) { model->baseRef = createDataItem(model->series); model->baseRef->setParent(model->series); model->baseRef->setVisible(false); } if (model->selectionRef == nullptr) { model->selectionRef = createDataItem(model->series); model->selectionRef->setParent(model->series); model->selectionRef->setVisible(false); } QQmlListReference baseRef(model->baseRef, "materials"); QQmlListReference selectionRef(model->selectionRef, "materials"); const QString &materialName = QStringLiteral(":/materials/ScatterMaterial"); if (!baseRef.size()) { auto mat = createQmlCustomMaterial(materialName); mat->setObjectName(materialName); mat->setParent(model->baseRef); baseRef.append(mat); } if (!selectionRef.size()) { auto mat = createQmlCustomMaterial(materialName); mat->setObjectName(materialName + QStringLiteral("_Selection")); mat->setParent(model->selectionRef); selectionRef.append(mat); } } void QQuickGraphsScatter::updateItemMaterial(QQuick3DModel *item, bool useGradient, bool rangeGradient, bool usePoint, const QString &materialName) { QQmlListReference materialsRef(item, "materials"); bool needNewMat = false; if (!materialsRef.size()) { needNewMat = true; } else if (materialsRef.at(0)->objectName().contains(QStringLiteral("Instancing")) != materialName.contains(QStringLiteral("Instancing"))) { needNewMat = true; } if (needNewMat) { materialsRef.clear(); auto newMaterial = createQmlCustomMaterial(materialName); newMaterial->setObjectName(materialName); newMaterial->setParent(item); materialsRef.append(newMaterial); } auto material = qobject_cast(materialsRef.at(0)); if (!useGradient) material->setProperty("colorStyle", 0); else if (!rangeGradient) material->setProperty("colorStyle", 1); else material->setProperty("colorStyle", 2); material->setProperty("usePoint", usePoint); material->setProperty("rootScale", rootNode()->scale().y()); } void QQuickGraphsScatter::updateInstancedMaterialProperties(ScatterModel *graphModel, const bool isHighlight, QQuick3DTexture *seriesTexture, QQuick3DTexture *highlightTexture, const bool transparency) { QQuick3DModel *model = nullptr; if (isHighlight) model = graphModel->selectionIndicator; else model = graphModel->instancingRootItem; QQmlListReference materialsRef(model, "materials"); auto customMaterial = static_cast(materialsRef.at(0)); customMaterial->setProperty("transparency", transparency); QVariant textureInputAsVariant = customMaterial->property("custex"); QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant .value(); if (isHighlight) { textureInput->setTexture(highlightTexture); if (selectedItemInSeries(graphModel->series)) m_selectedGradientPos = graphModel->instancing->customData().at(m_selectedItem); customMaterial->setProperty("gradientPos", m_selectedGradientPos); } else { textureInput->setTexture(seriesTexture); } } void QQuickGraphsScatter::updateMaterialProperties(QQuick3DModel *item, QQuick3DTexture *texture, QColor color, const bool transparency) { QQmlListReference materialsRef(item, "materials"); auto customMaterial = static_cast(materialsRef.at(0)); customMaterial->setProperty("transparency", transparency); customMaterial->setProperty("rootScale", rootNode()->scale().y()); int style = customMaterial->property("colorStyle").value(); if (style == 0) { customMaterial->setProperty("uColor", color); } else { QVariant textureInputAsVariant = customMaterial->property("custex"); QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant.value(); textureInput->setTexture(texture); const float scaleY = scaleWithBackground().y(); float rangeGradientYScaler = m_rangeGradientYHelper / scaleY; float value = (item->y() + scaleY) * rangeGradientYScaler; customMaterial->setProperty("gradientPos", value); } } QQuick3DTexture *QQuickGraphsScatter::createTexture() { QQuick3DTexture *texture = new QQuick3DTexture(); texture->setParent(this); texture->setRotationUV(-90.0f); texture->setHorizontalTiling(QQuick3DTexture::ClampToEdge); texture->setVerticalTiling(QQuick3DTexture::ClampToEdge); QQuickGraphsTextureData *textureData = new QQuickGraphsTextureData(); textureData->setParent(texture); textureData->setParentItem(texture); texture->setTextureData(textureData); return texture; } QQuick3DNode *QQuickGraphsScatter::createSeriesRoot() { auto model = new QQuick3DNode(); model->setParentItem(graphNode()); return model; } QQuick3DModel *QQuickGraphsScatter::createDataItem(QAbstract3DSeries *series) { auto model = new QQuick3DModel(); model->setParent(this); model->setParentItem(graphNode()); QString fileName = getMeshFileName(series); if (fileName.isEmpty()) fileName = series->userDefinedMesh(); model->setSource(QUrl(fileName)); return model; } void QQuickGraphsScatter::removeDataItems(ScatterModel *graphModel, QtGraphs3D::OptimizationHint optimizationHint) { if (optimizationHint == QtGraphs3D::OptimizationHint::Default) { delete graphModel->instancing; graphModel->instancing = nullptr; deleteDataItem(graphModel->instancingRootItem); deleteDataItem(graphModel->selectionIndicator); deleteDataItem(graphModel->baseRef); deleteDataItem(graphModel->selectionRef); deleteDataItem(graphModel->splineModel); graphModel->instancingRootItem = nullptr; graphModel->selectionIndicator = nullptr; graphModel->baseRef = nullptr; graphModel->selectionRef = nullptr; graphModel->splineModel = nullptr; } else { QList &items = graphModel->dataItems; removeDataItems(items, items.count()); } } void QQuickGraphsScatter::removeDataItems(QList &items, qsizetype count) { for (int i = 0; i < count; ++i) { QQuick3DModel *item = items.takeLast(); QQmlListReference materialsRef(item, "materials"); if (materialsRef.size()) { QObject *material = materialsRef.at(0); delete material; } item->deleteLater(); } } QList QQuickGraphsScatter::scatterSeriesList() { QList scatterSeriesList; for (QAbstract3DSeries *abstractSeries : m_seriesList) { QScatter3DSeries *scatterSeries = qobject_cast(abstractSeries); if (scatterSeries) scatterSeriesList.append(scatterSeries); } return scatterSeriesList; } void QQuickGraphsScatter::recreateDataItems() { if (!isComponentComplete()) return; QList seriesList = scatterSeriesList(); for (auto series : seriesList) { for (const auto &model : std::as_const(m_scatterGraphs)) { if (model->series == series) removeDataItems(model, optimizationHint()); } } markDataDirty(); } void QQuickGraphsScatter::recreateDataItems(const QList &graphs) { if (!isComponentComplete()) return; QList seriesList = scatterSeriesList(); for (auto series : seriesList) { for (const auto &model : graphs) { if (model->series == series) removeDataItems(model, optimizationHint()); } } markDataDirty(); } void QQuickGraphsScatter::addPointsToScatterModel(ScatterModel *graphModel, qsizetype count) { for (int i = 0; i < count; i++) { QQuick3DModel *item = createDataItem(graphModel->series); item->setPickable(true); item->setParent(graphModel->series); graphModel->dataItems.push_back(item); } setSeriesVisualsDirty(); } qsizetype QQuickGraphsScatter::sizeDifference(qsizetype size1, qsizetype size2) { return size2 - size1; } QVector3D QQuickGraphsScatter::selectedItemPosition() { QVector3D position; if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) position = {0.0f, 0.0f, 0.0f}; else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) position = {0.0f, 0.0f, 0.0f}; return position; } void QQuickGraphsScatter::fixMeshFileName(QString &fileName, QAbstract3DSeries *series) { auto meshType = series->mesh(); // Should it be smooth? if (series->isMeshSmooth() && meshType != QAbstract3DSeries::Mesh::Point && meshType != QAbstract3DSeries::Mesh::UserDefined) { fileName += QStringLiteral("Smooth"); } // Should it be filled? if (meshType != QAbstract3DSeries::Mesh::Sphere && meshType != QAbstract3DSeries::Mesh::Arrow && meshType != QAbstract3DSeries::Mesh::Minimal && meshType != QAbstract3DSeries::Mesh::Point && meshType != QAbstract3DSeries::Mesh::UserDefined) { fileName.append(QStringLiteral("Full")); } } QString QQuickGraphsScatter::getMeshFileName(QAbstract3DSeries *series) { QString fileName = {}; switch (series->mesh()) { case QAbstract3DSeries::Mesh::Sphere: fileName = QStringLiteral("defaultMeshes/sphereMesh"); break; case QAbstract3DSeries::Mesh::Bar: case QAbstract3DSeries::Mesh::Cube: fileName = QStringLiteral("defaultMeshes/barMesh"); break; case QAbstract3DSeries::Mesh::Pyramid: fileName = QStringLiteral("defaultMeshes/pyramidMesh"); break; case QAbstract3DSeries::Mesh::Cone: fileName = QStringLiteral("defaultMeshes/coneMesh"); break; case QAbstract3DSeries::Mesh::Cylinder: fileName = QStringLiteral("defaultMeshes/cylinderMesh"); break; case QAbstract3DSeries::Mesh::BevelBar: case QAbstract3DSeries::Mesh::BevelCube: fileName = QStringLiteral("defaultMeshes/bevelBarMesh"); break; case QAbstract3DSeries::Mesh::Minimal: fileName = QStringLiteral("defaultMeshes/minimalMesh"); break; case QAbstract3DSeries::Mesh::Arrow: fileName = QStringLiteral("defaultMeshes/arrowMesh"); break; case QAbstract3DSeries::Mesh::Point: fileName = shadowQuality() == QtGraphs3D::ShadowQuality::None ? QStringLiteral("defaultMeshes/planeMesh") : QStringLiteral("defaultMeshes/octagonMesh"); break; case QAbstract3DSeries::Mesh::UserDefined: break; default: fileName = QStringLiteral("defaultMeshes/sphereMesh"); } fixMeshFileName(fileName, series); return fileName; } void QQuickGraphsScatter::deleteDataItem(QQuick3DModel *item) { if (item) { QQmlListReference materialsRef(item, "materials"); if (materialsRef.size()) { QObject *material = materialsRef.at(0); delete material; } item->deleteLater(); item = nullptr; } } void QQuickGraphsScatter::handleSeriesChanged(QList changedSeries) { Q_UNUSED(changedSeries) // TODO: generate items and remove old items } bool QQuickGraphsScatter::selectedItemInSeries(const QScatter3DSeries *series) { return (m_selectedItem != -1 && m_selectedItemSeries == series); } bool QQuickGraphsScatter::isDotPositionInAxisRange(QVector3D dotPos) { return ((dotPos.x() >= axisX()->min() && dotPos.x() <= axisX()->max()) && (dotPos.y() >= axisY()->min() && dotPos.y() <= axisY()->max()) && (dotPos.z() >= axisZ()->min() && dotPos.z() <= axisZ()->max())); } QScatter3DSeries *QQuickGraphsScatter::selectedSeries() const { return m_selectedItemSeries; } void QQuickGraphsScatter::setSelectedItem(qsizetype index, QScatter3DSeries *series) { const QScatterDataProxy *proxy = 0; // Series may already have been removed, so check it before setting the selection. if (!m_seriesList.contains(series)) series = nullptr; if (series) proxy = series->dataProxy(); if (!proxy || index < 0 || index >= proxy->itemCount()) index = invalidSelectionIndex(); if (index != m_selectedItem || series != m_selectedItemSeries) { bool seriesChanged = (series != m_selectedItemSeries); // Clear hidden point from the previous selected series if (seriesChanged) { for (auto model : std::as_const(m_scatterGraphs)) { if (model->series && model->instancing && model->series == m_selectedItemSeries) model->instancing->unhidePreviousDataItem(); } } m_selectedItem = index; m_selectedItemSeries = series; m_changeTracker.selectedItemChanged = true; // Clear selection from other series and finally set new selection to the // specified series for (QAbstract3DSeries *otherSeries : m_seriesList) { QScatter3DSeries *scatterSeries = static_cast(otherSeries); if (scatterSeries != m_selectedItemSeries) scatterSeries->d_func()->setSelectedItem(invalidSelectionIndex()); } if (m_selectedItemSeries) m_selectedItemSeries->d_func()->setSelectedItem(m_selectedItem); if (seriesChanged) emit selectedSeriesChanged(m_selectedItemSeries); emitNeedRender(); } if (index != invalidSelectionIndex()) itemLabel()->setVisible(true); } void QQuickGraphsScatter::setSelectionMode(QtGraphs3D::SelectionFlags mode) { // We only support single item selection mode and no selection mode if (mode != QtGraphs3D::SelectionFlag::Item && mode != QtGraphs3D::SelectionFlag::None) { qWarning("Unsupported selection mode - only none and item selection modes " "are supported."); return; } QQuickGraphsItem::setSelectionMode(mode); } void QQuickGraphsScatter::handleAxisAutoAdjustRangeChangedInOrientation( QAbstract3DAxis::AxisOrientation orientation, bool autoAdjust) { Q_UNUSED(orientation); Q_UNUSED(autoAdjust); adjustAxisRanges(); } void QQuickGraphsScatter::handleAxisRangeChangedBySender(QObject *sender) { QQuickGraphsItem::handleAxisRangeChangedBySender(sender); m_isDataDirty = true; // Update selected index - may be moved offscreen setSelectedItem(m_selectedItem, m_selectedItemSeries); } QQmlListProperty QQuickGraphsScatter::seriesList() { return QQmlListProperty(this, this, &QQuickGraphsScatter::appendSeriesFunc, &QQuickGraphsScatter::countSeriesFunc, &QQuickGraphsScatter::atSeriesFunc, &QQuickGraphsScatter::clearSeriesFunc); } void QQuickGraphsScatter::appendSeriesFunc(QQmlListProperty *list, QScatter3DSeries *series) { reinterpret_cast(list->data)->addSeries(series); } qsizetype QQuickGraphsScatter::countSeriesFunc(QQmlListProperty *list) { return reinterpret_cast(list->data)->scatterSeriesList().size(); } QScatter3DSeries *QQuickGraphsScatter::atSeriesFunc(QQmlListProperty *list, qsizetype index) { return reinterpret_cast(list->data)->scatterSeriesList().at(index); } void QQuickGraphsScatter::clearSeriesFunc(QQmlListProperty *list) { QQuickGraphsScatter *declScatter = reinterpret_cast(list->data); QList realList = declScatter->scatterSeriesList(); qsizetype count = realList.size(); for (int i = 0; i < count; i++) declScatter->removeSeries(realList.at(i)); } void QQuickGraphsScatter::addSeries(QScatter3DSeries *series) { Q_ASSERT(series && series->type() == QAbstract3DSeries::SeriesType::Scatter); QQuickGraphsItem::addSeriesInternal(series); QScatter3DSeries *scatterSeries = static_cast(series); if (scatterSeries->selectedItem() != invalidSelectionIndex()) setSelectedItem(scatterSeries->selectedItem(), scatterSeries); auto graphModel = new ScatterModel; graphModel->series = series; graphModel->seriesTexture = nullptr; graphModel->highlightTexture = nullptr; m_scatterGraphs.push_back(graphModel); connectSeries(series); if (series->selectedItem() != invalidSelectionIndex()) setSelectedItem(series->selectedItem(), series); } void QQuickGraphsScatter::removeSeries(QScatter3DSeries *series) { bool wasVisible = (series && series->d_func()->m_graph == this && series->isVisible()); QQuickGraphsItem::removeSeriesInternal(series); if (m_selectedItemSeries == series) setSelectedItem(invalidSelectionIndex(), 0); if (wasVisible) adjustAxisRanges(); series->setParent(this); // Reparent as removing will leave series parentless // Find scattergraph model for (QList::ConstIterator it = m_scatterGraphs.cbegin(); it != m_scatterGraphs.cend();) { if ((*it)->series == series) { removeDataItems(*it, optimizationHint()); if ((*it)->seriesTexture) delete (*it)->seriesTexture; if ((*it)->highlightTexture) delete (*it)->highlightTexture; delete *it; it = m_scatterGraphs.erase(it); } else { ++it; } } disconnectSeries(series); } void QQuickGraphsScatter::handleAxisXChanged(QAbstract3DAxis *axis) { emit axisXChanged(static_cast(axis)); } void QQuickGraphsScatter::handleAxisYChanged(QAbstract3DAxis *axis) { emit axisYChanged(static_cast(axis)); } void QQuickGraphsScatter::handleAxisZChanged(QAbstract3DAxis *axis) { emit axisZChanged(static_cast(axis)); } void QQuickGraphsScatter::handleSeriesMeshChanged() { recreateDataItems(); } void QQuickGraphsScatter::handleMeshSmoothChanged(bool enable) { Q_UNUSED(enable); QScatter3DSeries *series = qobject_cast(sender()); for (auto &model : std::as_const(m_scatterGraphs)) { if (model->series == series) removeDataItems(model, optimizationHint()); } markDataDirty(); } void QQuickGraphsScatter::handleArrayReset() { QScatter3DSeries *series; if (qobject_cast(sender())) series = static_cast(sender())->series(); else series = static_cast(sender()); if (series->isVisible()) { adjustAxisRanges(); m_isDataDirty = true; } if (!m_changedSeriesList.contains(series)) m_changedSeriesList.append(series); setSelectedItem(m_selectedItem, m_selectedItemSeries); series->d_func()->markItemLabelDirty(); emitNeedRender(); } void QQuickGraphsScatter::handleItemsAdded(qsizetype startIndex, qsizetype count) { Q_UNUSED(startIndex); Q_UNUSED(count); QScatter3DSeries *series = static_cast(sender())->series(); if (series->isVisible()) { adjustAxisRanges(); m_isDataDirty = true; } if (!m_changedSeriesList.contains(series)) m_changedSeriesList.append(series); emitNeedRender(); } void QQuickGraphsScatter::handleItemsChanged(qsizetype startIndex, qsizetype count) { QScatter3DSeries *series = static_cast(sender())->series(); qsizetype oldChangeCount = m_changedItems.size(); if (!oldChangeCount) m_changedItems.reserve(count); for (int i = 0; i < count; i++) { bool newItem = true; qsizetype candidate = startIndex + i; for (qsizetype j = 0; j < oldChangeCount; j++) { const ChangeItem &oldChangeItem = m_changedItems.at(j); if (oldChangeItem.index == candidate && series == oldChangeItem.series) { newItem = false; break; } } if (newItem) { ChangeItem newChangeItem = {series, candidate}; m_changedItems.append(newChangeItem); if (series == m_selectedItemSeries && m_selectedItem == candidate) series->d_func()->markItemLabelDirty(); } } if (count) { m_changeTracker.itemChanged = true; if (series->isVisible()) adjustAxisRanges(); emitNeedRender(); } } void QQuickGraphsScatter::handleItemsRemoved(qsizetype startIndex, qsizetype count) { Q_UNUSED(startIndex); Q_UNUSED(count); QScatter3DSeries *series = static_cast(sender())->series(); if (series == m_selectedItemSeries) { // If items removed from selected series before the selection, adjust the selection qsizetype selectedItem = m_selectedItem; if (startIndex <= selectedItem) { if ((startIndex + count) > selectedItem) selectedItem = -1; // Selected item removed else selectedItem -= count; // Move selected item down by amount of item removed setSelectedItem(selectedItem, m_selectedItemSeries); } } if (series->isVisible()) { adjustAxisRanges(); m_isDataDirty = true; } if (!m_changedSeriesList.contains(series)) m_changedSeriesList.append(series); if (m_recordInsertsAndRemoves) { InsertRemoveRecord record(false, startIndex, count, series); m_insertRemoveRecords.append(record); } emitNeedRender(); } void QQuickGraphsScatter::adjustAxisRanges() { QValue3DAxis *valueAxisX = static_cast(m_axisX); QValue3DAxis *valueAxisY = static_cast(m_axisY); QValue3DAxis *valueAxisZ = static_cast(m_axisZ); bool adjustX = (valueAxisX && valueAxisX->isAutoAdjustRange()); bool adjustY = (valueAxisY && valueAxisY->isAutoAdjustRange()); bool adjustZ = (valueAxisZ && valueAxisZ->isAutoAdjustRange()); if (adjustX || adjustY || adjustZ) { float minValueX = 0.0f; float maxValueX = 0.0f; float minValueY = 0.0f; float maxValueY = 0.0f; float minValueZ = 0.0f; float maxValueZ = 0.0f; qsizetype seriesCount = m_seriesList.size(); for (int series = 0; series < seriesCount; series++) { const QScatter3DSeries *scatterSeries = static_cast( m_seriesList.at(series)); const QScatterDataProxy *proxy = scatterSeries->dataProxy(); if (scatterSeries->isVisible() && proxy) { QVector3D minLimits; QVector3D maxLimits; proxy->d_func()->limitValues(minLimits, maxLimits, valueAxisX, valueAxisY, valueAxisZ); if (adjustX) { if (!series) { // First series initializes the values minValueX = minLimits.x(); maxValueX = maxLimits.x(); } else { minValueX = qMin(minValueX, minLimits.x()); maxValueX = qMax(maxValueX, maxLimits.x()); } } if (adjustY) { if (!series) { // First series initializes the values minValueY = minLimits.y(); maxValueY = maxLimits.y(); } else { minValueY = qMin(minValueY, minLimits.y()); maxValueY = qMax(maxValueY, maxLimits.y()); } } if (adjustZ) { if (!series) { // First series initializes the values minValueZ = minLimits.z(); maxValueZ = maxLimits.z(); } else { minValueZ = qMin(minValueZ, minLimits.z()); maxValueZ = qMax(maxValueZ, maxLimits.z()); } } } } static const float adjustmentRatio = 20.0f; static const float defaultAdjustment = 1.0f; if (adjustX) { // If all points at same coordinate, need to default to some valid range float adjustment = 0.0f; if (minValueX == maxValueX) { if (adjustZ) { // X and Z are linked to have similar unit size, so choose the valid // range based on it if (minValueZ == maxValueZ) adjustment = defaultAdjustment; else adjustment = qAbs(maxValueZ - minValueZ) / adjustmentRatio; } else { if (valueAxisZ) adjustment = qAbs(valueAxisZ->max() - valueAxisZ->min()) / adjustmentRatio; else adjustment = defaultAdjustment; } } valueAxisX->d_func()->setRange(minValueX - adjustment, maxValueX + adjustment, true); } if (adjustY) { // If all points at same coordinate, need to default to some valid range // Y-axis unit is not dependent on other axes, so simply adjust +-1.0f float adjustment = 0.0f; if (minValueY == maxValueY) adjustment = defaultAdjustment; valueAxisY->d_func()->setRange(minValueY - adjustment, maxValueY + adjustment, true); } if (adjustZ) { // If all points at same coordinate, need to default to some valid range float adjustment = 0.0f; if (minValueZ == maxValueZ) { if (adjustX) { // X and Z are linked to have similar unit size, so choose the valid // range based on it if (minValueX == maxValueX) adjustment = defaultAdjustment; else adjustment = qAbs(maxValueX - minValueX) / adjustmentRatio; } else { if (valueAxisX) adjustment = qAbs(valueAxisX->max() - valueAxisX->min()) / adjustmentRatio; else adjustment = defaultAdjustment; } } valueAxisZ->d_func()->setRange(minValueZ - adjustment, maxValueZ + adjustment, true); } } } void QQuickGraphsScatter::handleItemsInserted(qsizetype startIndex, qsizetype count) { Q_UNUSED(startIndex); Q_UNUSED(count); QScatter3DSeries *series = static_cast(sender())->series(); if (series == m_selectedItemSeries) { // If items inserted to selected series before the selection, adjust the selection qsizetype selectedItem = m_selectedItem; if (startIndex <= selectedItem) { selectedItem += count; setSelectedItem(selectedItem, m_selectedItemSeries); } } if (series->isVisible()) { adjustAxisRanges(); m_isDataDirty = true; } if (!m_changedSeriesList.contains(series)) m_changedSeriesList.append(series); if (m_recordInsertsAndRemoves) { InsertRemoveRecord record(true, startIndex, count, series); m_insertRemoveRecords.append(record); } emitNeedRender(); } bool QQuickGraphsScatter::doPicking(QPointF position) { if (!QQuickGraphsItem::doPicking(position)) return false; if (selectionMode() == QtGraphs3D::SelectionFlag::Item) { QList results; if (m_customView) results = m_customView->pickAll(position.x(), position.y()); else results = pickAll(position.x(), position.y()); if (!results.empty()) { for (const auto &result : std::as_const(results)) { if (const auto &hitItem = result.objectHit()) { if (hitItem == backgroundBB() || hitItem == background()) { m_clickedType = QtGraphs3D::ElementType::None; clearSelectionModel(); continue; } if (m_clickedType != QtGraphs3D::ElementType::AxisXLabel && m_clickedType != QtGraphs3D::ElementType::AxisYLabel && m_clickedType != QtGraphs3D::ElementType::AxisZLabel) { if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) { setSelected(hitItem); handleSelectedElementChange(QtGraphs3D::ElementType::Series); break; } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) { setSelected(hitItem, result.instanceIndex()); handleSelectedElementChange(QtGraphs3D::ElementType::Series); break; } } else { clearSelectionModel(); } } } } else { clearSelectionModel(); handleSelectedElementChange(QtGraphs3D::ElementType::None); } } return true; } bool QQuickGraphsScatter::doRayPicking(QVector3D origin, QVector3D direction) { if (!QQuickGraphsItem::doRayPicking(origin, direction)) return false; if (selectionMode() == QtGraphs3D::SelectionFlag::Item) { QList results; if (m_customView) results = m_customView->rayPickAll(origin, direction); else results = rayPickAll(origin, direction); if (!results.empty()) { for (const auto &result : std::as_const(results)) { if (const auto &hit = result.objectHit()) { if (hit == backgroundBB() || hit == background()) { clearSelectionModel(); continue; } if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) { setSelected(hit); break; } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) { setSelected(hit, result.instanceIndex()); break; } } } } else { clearSelectionModel(); } } return true; } void QQuickGraphsScatter::updateShadowQuality(QtGraphs3D::ShadowQuality quality) { // Were shadows visible before? bool prevShadowsVisible = light()->castsShadow(); QQuickGraphsItem::updateShadowQuality(quality); setSeriesVisualsDirty(); if (prevShadowsVisible != light()->castsShadow()) { // Need to change mesh for series using point type QList graphs; for (const auto &graph : std::as_const(m_scatterGraphs)) { if (graph->series->mesh() == QAbstract3DSeries::Mesh::Point) graphs.append(graph); } recreateDataItems(graphs); } } void QQuickGraphsScatter::updateLightStrength() { for (auto graphModel : m_scatterGraphs) { for (const auto &obj : std::as_const(graphModel->dataItems)) { QQmlListReference materialsRef(obj, "materials"); auto material = qobject_cast(materialsRef.at(0)); material->setProperty("specularBrightness", lightStrength() * 0.05); } } } void QQuickGraphsScatter::startRecordingRemovesAndInserts() { m_recordInsertsAndRemoves = false; if (m_scene->selectionQueryPosition() != m_scene->invalidSelectionPoint()) { m_recordInsertsAndRemoves = true; if (m_insertRemoveRecords.size()) { m_insertRemoveRecords.clear(); // Reserve some space for remove/insert records to avoid unnecessary reallocations. m_insertRemoveRecords.reserve(insertRemoveRecordReserveSize); } } } void QQuickGraphsScatter::componentComplete() { QQuickGraphsItem::componentComplete(); QObject::connect(cameraTarget(), &QQuick3DNode::rotationChanged, this, &QQuickGraphsScatter::cameraRotationChanged); graphsInputHandler()->setGraphsItem(this); } void QQuickGraphsScatter::connectSeries(QScatter3DSeries *series) { QObject::connect(series, &QScatter3DSeries::meshChanged, this, &QQuickGraphsScatter::handleSeriesMeshChanged); QObject::connect(series, &QScatter3DSeries::meshSmoothChanged, this, &QQuickGraphsScatter::handleMeshSmoothChanged); QObject::connect(series, &QScatter3DSeries::itemSizeChanged, this, &QQuickGraphsScatter::markDataDirty); } void QQuickGraphsScatter::calculateSceneScalingFactors() { float marginV = 0.0f; float marginH = 0.0f; if (margin() < 0.0f) { if (m_maxItemSize > m_defaultMaxSize) marginH = m_maxItemSize / m_itemScaler; else marginH = m_defaultMaxSize; marginV = marginH; } else { marginH = margin(); marginV = margin(); } if (isPolar()) { float polarMargin = calculatePolarBackgroundMargin(); marginH = qMax(marginH, polarMargin); } float tHorizontalAspectRatio; if (isPolar()) tHorizontalAspectRatio = 1.0f; else tHorizontalAspectRatio = horizontalAspectRatio(); QSizeF areaSize; if (qFuzzyIsNull(tHorizontalAspectRatio)) { areaSize.setHeight(axisZ()->max() - axisZ()->min()); areaSize.setWidth(axisX()->max() - axisX()->min()); } else { areaSize.setHeight(1.0f); areaSize.setWidth(tHorizontalAspectRatio); } float horizontalMaxDimension; float scaleY = 0.0f; if (aspectRatio() > 2.0f) { horizontalMaxDimension = 2.0f; scaleY = 2.0f / aspectRatio(); } else { horizontalMaxDimension = aspectRatio(); scaleY = 1.0f; } if (isPolar()) m_polarRadius = horizontalMaxDimension; float scaleFactor = qMax(areaSize.width(), areaSize.height()); float scaleX = horizontalMaxDimension * areaSize.width() / scaleFactor; float scaleZ = horizontalMaxDimension * areaSize.height() / scaleFactor; setBackgroundScaleMargin({marginH, marginV, marginH}); setScaleWithBackground({scaleX, scaleY, scaleZ}); setScale({scaleX * 2.0f, scaleY * 2.0f, scaleZ * -2.0f}); setTranslate({-scaleX, -scaleY, scaleZ}); } float QQuickGraphsScatter::calculatePointScaleSize() { QList series = scatterSeriesList(); int totalDataSize = 0; for (const auto &scatterSeries : std::as_const(series)) { if (scatterSeries->isVisible()) totalDataSize += scatterSeries->dataArray().size(); } return qBound(m_defaultMinSize, 2.0f / float(qSqrt(qreal(totalDataSize))), m_defaultMaxSize); } void QQuickGraphsScatter::updatePointScaleSize() { m_pointScale = calculatePointScaleSize(); } void QQuickGraphsScatter::calculatePolarXZ(const float posX, const float posZ, float &x, float &z) const { const qreal angle = posX * (M_PI * 2.0f); const qreal radius = posZ; x = static_cast(radius * qSin(angle)) * m_polarRadius; z = -static_cast(radius * qCos(angle)) * m_polarRadius; } void QQuickGraphsScatter::updateSpline(ScatterModel *model) { if (auto series = qobject_cast(model->series)) { if (!series->isSplineVisible()) { if (model->splineModel) model->splineModel->setVisible(false); return; } else { if (!model->splineModel) createSplineModel(model); QQmlListReference materialRef(model->splineModel, "materials"); QQuick3DCustomMaterial *material = qobject_cast( materialRef.at(0)); QVariant splineInputAsVariant = material->property("controlPoints"); QQuick3DShaderUtilsTextureInput *splineInput = splineInputAsVariant.value(); QQuick3DTexture *splineTexture = splineInput->texture(); QQuick3DTextureData *splineData = splineTexture->textureData(); bool loop = series->isSplineLooping(); material->setProperty("tension", series->splineTension()); material->setProperty("knotting", series->splineKnotting()); material->setProperty("loop", loop); material->setProperty("color", series->splineColor()); const QScatterDataArray &array = series->dataArray(); qsizetype pointCount = array.size(); if (isDataDirty() && array.size() != 0) { QVector splinePoints; QVector vertices; splinePoints.reserve(pointCount + 2); splineData->setSize(QSize(pointCount + 2, 1)); auto normalizedPos = [this](QVector3D pos) { float posX = static_cast(axisX())->positionAt(pos.x()) * scale().x() + translate().x(); float posY = static_cast(axisY())->positionAt(pos.y()) * scale().y() + translate().y(); float posZ = static_cast(axisZ())->positionAt(pos.z()) * scale().z() + translate().z(); return QVector3D(posX, posY, posZ); }; QVector3D first = normalizedPos(array.at(0).position()); QVector3D second = normalizedPos(array.at(1).position()); QVector3D pStart = first + (first - second) * 0.1f; QVector3D last = normalizedPos(array.at(pointCount - 1).position()); QVector3D secondLast = normalizedPos(array.at(pointCount - 2).position()); QVector3D pEnd = last + (last - secondLast) * 0.1f; if (loop) splinePoints.append(QVector4D(last, 1)); else splinePoints.append(QVector4D(pStart, 1)); const qsizetype resolution = series->splineResolution(); vertices.reserve(resolution * pointCount); for (int i = 0; i < pointCount; i++) { splinePoints.push_back(QVector4D(normalizedPos(array.at(i).position()), 1)); for (int j = 0; j < resolution; j++) { SplineVertex vertex; vertex.position = QVector3D(float(j) / float(resolution), float(i), 0); vertex.uv = QVector2D(float(j) / float(resolution - 1), float(i) / float(pointCount + 2)); vertices.push_back(vertex); } } if (loop) splinePoints.append(QVector4D(first, 1)); else splinePoints.append(QVector4D(pEnd, 1)); QByteArray pointData = QByteArray(reinterpret_cast(splinePoints.data()), splinePoints.size() * sizeof(QVector4D)); splineData->setTextureData(pointData); material->setProperty("points", splinePoints.size()); QQuick3DGeometry *splineGeometry = model->splineModel->geometry(); QByteArray vertexBuffer(reinterpret_cast(vertices.data()), vertices.size() * sizeof(SplineVertex)); splineGeometry->setVertexData(vertexBuffer); splineGeometry->update(); splineTexture->setTextureData(splineData); splineInput->setTexture(splineTexture); } model->splineModel->setVisible(true); } } } void QQuickGraphsScatter::createSplineModel(ScatterModel *model) { QQuick3DModel *splineModel = new QQuick3DModel(); splineModel->setParent(model->series); splineModel->setParentItem(graphNode()); splineModel->setObjectName(QStringLiteral("SplineModel")); splineModel->setVisible(true); splineModel->setPickable(false); auto geometry = new QQuick3DGeometry(); geometry->setParent(splineModel); geometry->setStride(sizeof(SplineVertex)); //pos + uv geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::LineStrip); geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type); geometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic, sizeof(QVector3D), QQuick3DGeometry::Attribute::F32Type); splineModel->setGeometry(geometry); QQuick3DTexture *splineTex = new QQuick3DTexture(); splineTex->setHorizontalTiling(QQuick3DTexture::ClampToEdge); splineTex->setVerticalTiling(QQuick3DTexture::ClampToEdge); splineTex->setMinFilter(QQuick3DTexture::Nearest); splineTex->setMagFilter(QQuick3DTexture::Nearest); QQuick3DTextureData *splineData = new QQuick3DTextureData; splineData->setSize(QSize(0, 1)); splineData->setFormat(QQuick3DTextureData::RGBA32F); splineData->setParent(splineTex); splineData->setParentItem(splineTex); splineTex->setTextureData(splineData); QQmlListReference materialRef(splineModel, "materials"); QQuick3DCustomMaterial *material = createQmlCustomMaterial( QStringLiteral(":/materials/SplineMaterial")); material->setParent(splineModel); material->setParentItem(splineModel); material->setObjectName("splineMaterial"); QVariant textureInputAsVariant = material->property("controlPoints"); QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant .value(); textureInput->setTexture(splineTex); splineTex->setParent(material); materialRef.append(material); model->splineModel = splineModel; if (auto series = qobject_cast(model->series)) { connect(series, &QSpline3DSeries::splineTensionChanged, this, &QQuickGraphsScatter::handleSplineChanged); connect(series, &QSpline3DSeries::splineKnottingChanged, this, &QQuickGraphsScatter::handleSplineChanged); connect(series, &QSpline3DSeries::splineLoopingChanged, this, &QQuickGraphsScatter::handleSplineChanged); connect(series, &QSpline3DSeries::splineColorChanged, this, &QQuickGraphsScatter::handleSplineChanged); connect(series, &QSpline3DSeries::splineResolutionChanged, this, &QQuickGraphsScatter::handleSplineChanged); } } void QQuickGraphsScatter::handleSplineChanged() { m_isDataDirty = true; } QQuick3DModel *QQuickGraphsScatter::selected() const { return m_selected; } void QQuickGraphsScatter::setSelected(QQuick3DModel *newSelected) { if (newSelected != m_selected) { m_previousSelected = m_selected; m_selected = newSelected; auto series = static_cast(m_selected->parent()); // Find scattermodel ScatterModel *graphModel = nullptr; for (const auto &model : std::as_const(m_scatterGraphs)) { if (model->series == series) { graphModel = model; break; } } if (graphModel) { qsizetype index = graphModel->dataItems.indexOf(m_selected); setSelectedItem(index, series); setSeriesVisualsDirty(); setSelectedItemChanged(true); } } } void QQuickGraphsScatter::setSelected(QQuick3DModel *root, qsizetype index) { auto series = static_cast(root->parent()); if (index != m_selectedItem || series != m_selectedItemSeries) { setSeriesVisualsDirty(); setSelectedItem(index, series); setSelectedItemChanged(true); } } void QQuickGraphsScatter::clearSelectionModel() { if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) clearAllSelectionInstanced(); setSelectedItem(invalidSelectionIndex(), nullptr); if (itemLabel()) itemLabel()->setVisible(false); setSeriesVisualsDirty(); m_selected = nullptr; m_previousSelected = nullptr; } void QQuickGraphsScatter::clearAllSelectionInstanced() { for (const auto &graph : m_scatterGraphs) { if (graph->instancing) graph->instancing->resetVisibilty(); } } void QQuickGraphsScatter::optimizationChanged(QtGraphs3D::OptimizationHint toOptimization) { if (toOptimization == QtGraphs3D::OptimizationHint::Default) { for (const auto &graph : std::as_const(m_scatterGraphs)) removeDataItems(graph, QtGraphs3D::OptimizationHint::Legacy); } else { for (const auto &graph : std::as_const(m_scatterGraphs)) removeDataItems(graph, QtGraphs3D::OptimizationHint::Default); } setSeriesVisualsDirty(); } void QQuickGraphsScatter::updateGraph() { updatePointScaleSize(); if (m_optimizationChanged) { optimizationChanged(optimizationHint()); m_optimizationChanged = false; } for (auto graphModel : std::as_const(m_scatterGraphs)) { bool seriesVisible = graphModel->series->isVisible(); if (isDataDirty()) { if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy && seriesVisible) { if (graphModel->dataItems.count() != graphModel->series->dataProxy()->itemCount()) { qsizetype sizeDiff = sizeDifference(graphModel->dataItems.count(), graphModel->series->dataProxy()->itemCount()); if (sizeDiff > 0) addPointsToScatterModel(graphModel, sizeDiff); else removeDataItems(graphModel->dataItems, qAbs(sizeDiff)); } } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default && seriesVisible) { if (graphModel->instancing == nullptr) { graphModel->instancing = new ScatterInstancing; graphModel->instancing->setParent(graphModel->series); } if (graphModel->instancingRootItem == nullptr) { graphModel->instancingRootItem = createDataItem(graphModel->series); graphModel->instancingRootItem->setParent(graphModel->series); graphModel->instancingRootItem->setInstancing(graphModel->instancing); if (selectionMode() != QtGraphs3D::SelectionFlag::None) { graphModel->instancingRootItem->setPickable(true); graphModel->selectionIndicator = createDataItem(graphModel->series); graphModel->selectionIndicator->setVisible(false); } } } } if (seriesVisible && (isDataDirty() || isSeriesVisualsDirty())) { updateScatterGraphItemPositions(graphModel); updateSpline(graphModel); } if (seriesVisible && (isSeriesVisualsDirty() || (graphModel->instancing && graphModel->instancing->isDirty()))) { updateScatterGraphItemVisuals(graphModel); } const bool validSelection = (m_selectedItemSeries == graphModel->series && m_selectedItem != invalidSelectionIndex()) && selectedItemInRange(graphModel); if (validSelection) { QVector3D selectionPosition = {0.0f, 0.0f, 0.0f}; if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) { QQuick3DModel *selectedModel = graphModel->dataItems.at(m_selectedItem); selectionPosition = selectedModel->position(); } else { selectionPosition = graphModel->instancing->dataArray().at(m_selectedItem).position; } updateItemLabel(selectionPosition); QString label = m_selectedItemSeries->itemLabel(); itemLabel()->setProperty("labelText", label); if (!label.compare(hiddenLabelTag)) itemLabel()->setVisible(false); } } if (m_selectedItem == invalidSelectionIndex()) itemLabel()->setVisible(false); setItemSelected(m_selectedItem != invalidSelectionIndex()); } void QQuickGraphsScatter::synchData() { QList seriesList = scatterSeriesList(); float maxItemSize = 0.0f; for (const auto &series : std::as_const(seriesList)) { if (series->isVisible()) { float itemSize = series->itemSize(); if (itemSize > maxItemSize) maxItemSize = itemSize; } } m_maxItemSize = maxItemSize; updatePointScaleSize(); QQuickGraphsItem::synchData(); m_pointScale = calculatePointScaleSize(); if (hasSelectedItemChanged()) { if (m_selectedItem != invalidSelectionIndex()) { QString itemLabelText = m_selectedItemSeries->itemLabel(); itemLabel()->setProperty("labelText", itemLabelText); if (!itemLabelText.compare(hiddenLabelTag)) itemLabel()->setVisible(false); } setSelectedItemChanged(false); } } void QQuickGraphsScatter::cameraRotationChanged() { m_isDataDirty = true; } void QQuickGraphsScatter::handleOptimizationHintChange(QtGraphs3D::OptimizationHint hint) { Q_UNUSED(hint) m_optimizationChanged = true; } void QQuickGraphsScatter::handleSeriesVisibilityChangedBySender(QObject *sender) { QQuickGraphsItem::handleSeriesVisibilityChangedBySender(sender); auto series = static_cast(sender); if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) { ScatterModel *graphModel = findGraphModel(series); if (graphModel && graphModel->series == series) { if (graphModel->instancingRootItem) graphModel->instancingRootItem->setVisible(series->isVisible()); if (series == m_selectedItemSeries) { itemLabel()->setVisible(series->isVisible()); graphModel->selectionIndicator->setVisible(series->isVisible()); } } } } bool QQuickGraphsScatter::selectedItemInRange(const ScatterModel *graphModel) { qsizetype itemCount; if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) itemCount = graphModel->instancing->dataArray().count(); else itemCount = graphModel->dataItems.count(); return m_selectedItem >= 0 && m_selectedItem < itemCount; } QQuickGraphsScatter::ScatterModel *QQuickGraphsScatter::findGraphModel(QScatter3DSeries *series) { for (const auto &graphModel : std::as_const(m_scatterGraphs)) { if (graphModel->series == series) return graphModel; } return nullptr; } QT_END_NAMESPACE