1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "q3dscene.h"
5#include "qgraphsinputhandler_p.h"
6#include "qquickgraphsscatter_p.h"
7#include "qquickgraphstexturedata_p.h"
8#include "qscatter3dseries_p.h"
9#include "qscatterdataproxy_p.h"
10#include "qvalue3daxis_p.h"
11
12#include <QColor>
13#include <QtCore/QMutexLocker>
14#include <QtQuick3D/private/qquick3dcustommaterial_p.h>
15#include <QtQuick3D/private/qquick3ddirectionallight_p.h>
16#include <QtQuick3D/private/qquick3dmodel_p.h>
17#include <QtQuick3D/private/qquick3dperspectivecamera_p.h>
18#include <QtQuick3D/private/qquick3dpointlight_p.h>
19#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
20#include <QtQuick3D/private/qquick3drepeater_p.h>
21
22QT_BEGIN_NAMESPACE
23
24static const int insertRemoveRecordReserveSize = 31;
25
26/*!
27 * \qmltype Scatter3D
28 * \inherits GraphsItem3D
29 * \inqmlmodule QtGraphs
30 * \ingroup graphs_qml_3D
31 * \brief 3D scatter graph.
32 *
33 * This type enables developers to render scatter graphs in 3D with Qt Quick.
34 *
35 * You will need to import Qt Graphs module to use this type:
36 *
37 * \snippet doc_src_qmlgraphs.cpp 0
38 *
39 * After that you can use Scatter3D in your qml files:
40 *
41 * \snippet doc_src_qmlgraphs.cpp 2
42 *
43 * See \l{Simple Scatter Graph} for more thorough usage example.
44 *
45 * \sa Scatter3DSeries, ScatterDataProxy, Bars3D, Surface3D,
46 * {Qt Graphs C++ Classes for 3D}
47 */
48
49/*!
50 * \qmlproperty Value3DAxis Scatter3D::axisX
51 * The active x-axis.
52 *
53 * If an axis is not given, a temporary default axis with no labels and an
54 * automatically adjusting range is created.
55 * This temporary axis is destroyed if another axis is explicitly set to the
56 * same orientation.
57 */
58
59/*!
60 * \qmlproperty Value3DAxis Scatter3D::axisY
61 * The active y-axis.
62 *
63 * If an axis is not given, a temporary default axis with no labels and an
64 * automatically adjusting range is created.
65 * This temporary axis is destroyed if another axis is explicitly set to the
66 * same orientation.
67 */
68
69/*!
70 * \qmlproperty Value3DAxis Scatter3D::axisZ
71 * The active z-axis.
72 *
73 * If an axis is not given, a temporary default axis with no labels and an
74 * automatically adjusting range is created.
75 * This temporary axis is destroyed if another axis is explicitly set to the
76 * same orientation.
77 */
78
79/*!
80 * \qmlproperty Scatter3DSeries Scatter3D::selectedSeries
81 * \readonly
82 *
83 * The selected series or null.
84 */
85
86/*!
87 * \qmlproperty list<Scatter3DSeries> Scatter3D::seriesList
88 * \qmldefault
89 * This property holds the series of the graph.
90 * By default, this property contains an empty list.
91 * To set the series, either use the addSeries() method or define them as
92 * children of the graph.
93 */
94
95/*!
96 * \qmlmethod void Scatter3D::addSeries(Scatter3DSeries series)
97 * Adds the \a series to the graph. A graph can contain multiple series, but has
98 * only one set of axes. If the newly added series has specified a selected
99 * item, it will be highlighted and any existing selection will be cleared. Only
100 * one added series can have an active selection.
101 * \sa GraphsItem3D::hasSeries()
102 */
103
104/*!
105 * \qmlmethod void Scatter3D::removeSeries(Scatter3DSeries series)
106 * Remove the \a series from the graph.
107 * \sa GraphsItem3D::hasSeries()
108 */
109
110/*!
111 * \qmlsignal Scatter3D::axisXChanged(ValueAxis3D axis)
112 *
113 * This signal is emitted when axisX changes to \a axis.
114*/
115
116/*!
117 * \qmlsignal Scatter3D::axisYChanged(ValueAxis3D axis)
118 *
119 * This signal is emitted when axisY changes to \a axis.
120*/
121
122/*!
123 * \qmlsignal Scatter3D::axisZChanged(ValueAxis3D axis)
124 *
125 * This signal is emitted when axisZ changes to \a axis.
126*/
127
128/*!
129 * \qmlsignal Scatter3D::selectedSeriesChanged(Scatter3DSeries series)
130 *
131 * This signal is emitted when selectedSeries changes to \a series.
132*/
133
134QQuickGraphsScatter::QQuickGraphsScatter(QQuickItem *parent)
135 : QQuickGraphsItem(parent)
136{
137 m_graphType = QAbstract3DSeries::SeriesType::Scatter;
138 setAxisX(0);
139 setAxisY(0);
140 setAxisZ(0);
141 setAcceptedMouseButtons(Qt::AllButtons);
142 setFlag(flag: ItemHasContents);
143 clearSelection();
144}
145
146QQuickGraphsScatter::~QQuickGraphsScatter()
147{
148 QMutexLocker locker(m_nodeMutex.data());
149 const QMutexLocker locker2(mutex());
150
151 for (auto &graphModel : m_scatterGraphs) {
152 delete graphModel;
153 }
154}
155
156void QQuickGraphsScatter::setAxisX(QValue3DAxis *axis)
157{
158 QQuickGraphsItem::setAxisX(axis);
159}
160
161QValue3DAxis *QQuickGraphsScatter::axisX() const
162{
163 return static_cast<QValue3DAxis *>(QQuickGraphsItem::axisX());
164}
165
166void QQuickGraphsScatter::setAxisY(QValue3DAxis *axis)
167{
168 QQuickGraphsItem::setAxisY(axis);
169}
170
171QValue3DAxis *QQuickGraphsScatter::axisY() const
172{
173 return static_cast<QValue3DAxis *>(QQuickGraphsItem::axisY());
174}
175
176void QQuickGraphsScatter::setAxisZ(QValue3DAxis *axis)
177{
178 QQuickGraphsItem::setAxisZ(axis);
179}
180
181QValue3DAxis *QQuickGraphsScatter::axisZ() const
182{
183 return static_cast<QValue3DAxis *>(QQuickGraphsItem::axisZ());
184}
185
186void QQuickGraphsScatter::disconnectSeries(QScatter3DSeries *series)
187{
188 QObject::disconnect(sender: series, signal: 0, receiver: this, member: 0);
189}
190
191void QQuickGraphsScatter::generatePointsForScatterModel(ScatterModel *graphModel)
192{
193 QList<QQuick3DModel *> itemList;
194 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) {
195 qsizetype itemCount = graphModel->series->dataProxy()->itemCount();
196 if (graphModel->series->dataProxy()->itemCount() > 0)
197 itemList.resize(size: itemCount);
198
199 for (int i = 0; i < itemCount; i++) {
200 QQuick3DModel *item = createDataItem(series: graphModel->series);
201 item->setPickable(true);
202 item->setParent(graphModel->series);
203 itemList[i] = item;
204 }
205 graphModel->dataItems = itemList;
206 markDataDirty();
207 } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) {
208 graphModel->instancingRootItem = createDataItem(series: graphModel->series);
209 graphModel->instancingRootItem->setParent(graphModel->series);
210 graphModel->instancingRootItem->setInstancing(graphModel->instancing);
211 if (selectionMode() != QtGraphs3D::SelectionFlag::None) {
212 graphModel->selectionIndicator = createDataItem(series: graphModel->series);
213 graphModel->instancingRootItem->setPickable(true);
214 }
215 }
216 markSeriesVisualsDirty();
217}
218
219qsizetype QQuickGraphsScatter::getItemIndex(QQuick3DModel *item)
220{
221 Q_UNUSED(item);
222 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy)
223 return 0;
224
225 return -1;
226}
227
228void QQuickGraphsScatter::clearSelection()
229{
230 clearSelectionModel();
231}
232
233void QQuickGraphsScatter::updateScatterGraphItemPositions(ScatterModel *graphModel)
234{
235 float itemSize = graphModel->series->itemSize() / m_itemScaler;
236 QQuaternion meshRotation = graphModel->series->meshRotation();
237 QScatterDataProxy *dataProxy = graphModel->series->dataProxy();
238 QList<QQuick3DModel *> itemList = graphModel->dataItems;
239
240 auto valueAxisX = static_cast<QValue3DAxis *>(axisX());
241 auto valueAxisY = static_cast<QValue3DAxis *>(axisY());
242 auto valueAxisZ = static_cast<QValue3DAxis *>(axisZ());
243 bool xReversed = valueAxisX->reversed();
244 bool yReversed = valueAxisY->reversed();
245 bool zReversed = valueAxisZ->reversed();
246
247 if (itemSize == 0.0f)
248 itemSize = m_pointScale;
249
250 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) {
251 if (dataProxy->itemCount() != itemList.size()) {
252 qWarning(msg: "%ls Item count differs from itemList count",
253 qUtf16Printable(QString::fromUtf8(__func__)));
254 }
255
256 for (int i = 0; i < dataProxy->itemCount(); ++i) {
257 const QScatterDataItem item = dataProxy->itemAt(index: i);
258 QQuick3DModel *dataPoint = itemList.at(i);
259
260 QVector3D dotPos = item.position();
261 if (isDotPositionInAxisRange(dotPos)) {
262 dataPoint->setVisible(true);
263 QQuaternion dotRot = item.rotation();
264 float dotPosX = xReversed ? 1.0f - valueAxisX->positionAt(x: dotPos.x())
265 : valueAxisX->positionAt(x: dotPos.x());
266 float dotPosY = yReversed ? 1.0f - valueAxisY->positionAt(x: dotPos.y())
267 : valueAxisY->positionAt(x: dotPos.y());
268 float dotPosZ = zReversed ? 1.0f - valueAxisZ->positionAt(x: dotPos.z())
269 : valueAxisZ->positionAt(x: dotPos.z());
270
271 float posX = dotPosX * scale().x() + translate().x();
272 float posY = dotPosY * scale().y() + translate().y();
273 float posZ = dotPosZ * scale().z() + translate().z();
274 dataPoint->setPosition(QVector3D(posX, posY, posZ));
275 QQuaternion totalRotation;
276
277 if (graphModel->series->mesh() != QAbstract3DSeries::Mesh::Point)
278 totalRotation = dotRot * meshRotation;
279 else
280 totalRotation = cameraTarget()->rotation();
281
282 dataPoint->setRotation(totalRotation);
283 dataPoint->setScale(QVector3D(itemSize, itemSize, itemSize));
284 } else {
285 dataPoint->setVisible(false);
286 }
287 }
288 } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) {
289 qsizetype count = dataProxy->itemCount();
290 QList<DataItemHolder> positions;
291
292 for (int i = 0; i < count; i++) {
293 const QScatterDataItem &item = dataProxy->itemAt(index: i);
294 QVector3D dotPos = item.position();
295
296 if (isDotPositionInAxisRange(dotPos)) {
297 float dotPosX = xReversed ? 1.0f - valueAxisX->positionAt(x: dotPos.x())
298 : valueAxisX->positionAt(x: dotPos.x());
299 float dotPosY = yReversed ? 1.0f - valueAxisY->positionAt(x: dotPos.y())
300 : valueAxisY->positionAt(x: dotPos.y());
301 float dotPosZ = zReversed ? 1.0f - valueAxisZ->positionAt(x: dotPos.z())
302 : valueAxisZ->positionAt(x: dotPos.z());
303
304 float posX = dotPosX * scale().x() + translate().x();
305 float posY = dotPosY * scale().y() + translate().y();
306 float posZ = dotPosZ * scale().z() + translate().z();
307
308 QQuaternion totalRotation;
309
310 if (graphModel->series->mesh() != QAbstract3DSeries::Mesh::Point)
311 totalRotation = item.rotation() * meshRotation;
312 else
313 totalRotation = cameraTarget()->rotation();
314
315 DataItemHolder dih;
316 if (isPolar()) {
317 float x;
318 float z;
319 calculatePolarXZ(posX: axisX()->positionAt(x: dotPos.x()),
320 posZ: axisZ()->positionAt(x: dotPos.z()),
321 x,
322 z);
323 dih.position = {x, posY, z};
324 } else {
325 dih.position = {posX, posY, posZ};
326 }
327 dih.rotation = totalRotation;
328 dih.scale = {itemSize, itemSize, itemSize};
329
330 positions.push_back(t: dih);
331 } else {
332 DataItemHolder dih;
333 dih.hide = true;
334 positions.push_back(t: dih);
335 }
336 }
337 graphModel->instancing->setDataArray(positions);
338
339 if (selectedItemInSeries(series: graphModel->series)) {
340 if (isDotPositionInAxisRange(dotPos: dataProxy->itemAt(index: m_selectedItem).position())) {
341 QQuaternion totalRotation;
342
343 if (graphModel->series->mesh() != QAbstract3DSeries::Mesh::Point) {
344 totalRotation = graphModel->instancing->dataArray().at(i: m_selectedItem).rotation
345 * meshRotation;
346 } else {
347 totalRotation = cameraTarget()->rotation();
348 }
349 graphModel->selectionIndicator->setRotation(totalRotation);
350 graphModel->instancing->hideDataItem(index: m_selectedItem);
351 } else {
352 clearSelectionModel();
353 }
354 }
355 }
356}
357
358void QQuickGraphsScatter::updateScatterGraphItemVisuals(ScatterModel *graphModel)
359{
360 bool useGradient = graphModel->series->d_func()->isUsingGradient();
361 bool usePoint = graphModel->series->mesh() == QAbstract3DSeries::Mesh::Point;
362 qsizetype itemCount = graphModel->series->dataProxy()->itemCount();
363
364 if (useGradient) {
365 if (!graphModel->seriesTexture) {
366 graphModel->seriesTexture = createTexture();
367 graphModel->seriesTexture->setParent(graphModel->series);
368 }
369
370 QLinearGradient gradient = graphModel->series->baseGradient();
371 auto textureData = static_cast<QQuickGraphsTextureData *>(
372 graphModel->seriesTexture->textureData());
373 textureData->createGradient(gradient);
374
375 if (!graphModel->highlightTexture) {
376 graphModel->highlightTexture = createTexture();
377 graphModel->highlightTexture->setParent(graphModel->series);
378 }
379
380 QLinearGradient highlightGradient = graphModel->series->singleHighlightGradient();
381 auto highlightTextureData = static_cast<QQuickGraphsTextureData *>(
382 graphModel->highlightTexture->textureData());
383 highlightTextureData->createGradient(gradient: highlightGradient);
384 } else {
385 if (graphModel->seriesTexture) {
386 graphModel->seriesTexture->deleteLater();
387 graphModel->seriesTexture = nullptr;
388 }
389
390 if (graphModel->highlightTexture) {
391 graphModel->highlightTexture->deleteLater();
392 graphModel->highlightTexture = nullptr;
393 }
394 }
395
396 bool rangeGradient = (useGradient
397 && graphModel->series->d_func()->m_colorStyle
398 == QGraphsTheme::ColorStyle::RangeGradient)
399 ? true
400 : false;
401
402 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) {
403 // Release resources that might not have been deleted even though deleteLater had been set
404 window()->releaseResources();
405
406 if (itemCount != graphModel->dataItems.size())
407 qWarning(msg: "%ls Item count differs from itemList count",
408 qUtf16Printable(QString::fromUtf8(__func__)));
409
410 bool transparentTexture = false;
411 if (graphModel->seriesTexture) {
412 auto textureData = static_cast<QQuickGraphsTextureData *>(
413 graphModel->seriesTexture->textureData());
414 transparentTexture = textureData->hasTransparency();
415 }
416 const bool transparency = (graphModel->series->baseColor().alphaF() < 1.0)
417 || transparentTexture;
418
419 updateMaterialReference(model: graphModel);
420 QQmlListReference baseRef(graphModel->baseRef, "materials");
421 auto baseMat = static_cast<QQuick3DCustomMaterial *>(baseRef.at(0));
422 for (const auto &obj : std::as_const(t&: graphModel->dataItems)) {
423 QQmlListReference matRef(obj, "materials");
424 matRef.clear();
425 matRef.append(baseMat);
426 }
427 if (m_selectedItem != invalidSelectionIndex() && graphModel->series == selectedSeries()) {
428 QQmlListReference selRef(graphModel->selectionRef, "materials");
429 auto selMat = static_cast<QQuick3DCustomMaterial *>(selRef.at(0));
430 QQuick3DModel *selectedItem = graphModel->dataItems.at(i: m_selectedItem);
431 QQmlListReference matRef(selectedItem, "materials");
432 matRef.clear();
433 matRef.append(selMat);
434 }
435 updateItemMaterial(item: graphModel->baseRef,
436 useGradient,
437 rangeGradient,
438 usePoint,
439 QStringLiteral(":/materials/ScatterMaterial"));
440
441 updateItemMaterial(item: graphModel->selectionRef,
442 useGradient,
443 rangeGradient,
444 usePoint,
445 QStringLiteral(":/materials/ScatterMaterial"));
446 updateMaterialProperties(item: graphModel->baseRef,
447 texture: graphModel->seriesTexture,
448 color: graphModel->series->baseColor(),
449 transparency);
450
451 updateMaterialProperties(item: graphModel->selectionRef,
452 texture: graphModel->highlightTexture,
453 color: graphModel->series->singleHighlightColor());
454
455 } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) {
456 graphModel->instancingRootItem->setVisible(true);
457 graphModel->instancing->setRangeGradient(rangeGradient);
458 if (!rangeGradient) {
459 bool transparentTexture = false;
460 if (graphModel->seriesTexture) {
461 auto textureData = static_cast<QQuickGraphsTextureData *>(
462 graphModel->seriesTexture->textureData());
463 transparentTexture = textureData->hasTransparency();
464 }
465 const bool transparency = (graphModel->series->baseColor().alphaF() < 1.0)
466 || transparentTexture;
467 if (transparency)
468 graphModel->instancing->setTransparency(true);
469 else
470 graphModel->instancing->setTransparency(false);
471
472 updateItemMaterial(item: graphModel->instancingRootItem,
473 useGradient,
474 rangeGradient,
475 usePoint,
476 QStringLiteral(":/materials/ScatterMaterialInstancing"));
477 updateMaterialProperties(item: graphModel->instancingRootItem,
478 texture: graphModel->seriesTexture,
479 color: graphModel->series->baseColor(),
480 transparency);
481 } else {
482 auto textureData = static_cast<QQuickGraphsTextureData *>(
483 graphModel->seriesTexture->textureData());
484 if (textureData->hasTransparency())
485 graphModel->instancing->setTransparency(true);
486 else
487 graphModel->instancing->setTransparency(false);
488
489 updateItemMaterial(item: graphModel->instancingRootItem,
490 useGradient,
491 rangeGradient,
492 usePoint,
493 QStringLiteral(":/materials/ScatterMaterialInstancing"));
494 updateInstancedMaterialProperties(graphModel,
495 isHighlight: false,
496 seriesTexture: graphModel->seriesTexture,
497 highlightTexture: graphModel->highlightTexture,
498 transparency: textureData->hasTransparency());
499
500 const float scaleY = scaleWithBackground().y();
501 float rangeGradientYScaler = m_rangeGradientYHelper / scaleY;
502
503 QList<float> customData;
504 customData.resize(size: itemCount);
505
506 QList<DataItemHolder> instancingData = graphModel->instancing->dataArray();
507 for (int i = 0; i < instancingData.size(); i++) {
508 auto dih = instancingData.at(i);
509 float value = (dih.position.y() + scaleY) * rangeGradientYScaler;
510 customData[i] = value;
511 }
512 graphModel->instancing->setCustomData(customData);
513 }
514
515 if (selectedItemInSeries(series: graphModel->series)) {
516 // Selection indicator
517 if (!rangeGradient) {
518 updateItemMaterial(item: graphModel->selectionIndicator,
519 useGradient,
520 rangeGradient,
521 usePoint,
522 QStringLiteral(":/materials/ScatterMaterial"));
523 updateMaterialProperties(item: graphModel->selectionIndicator,
524 texture: graphModel->highlightTexture,
525 color: graphModel->series->singleHighlightColor());
526 graphModel->selectionIndicator->setCastsShadows(!usePoint);
527 } else {
528 // Rangegradient
529 updateItemMaterial(item: graphModel->selectionIndicator,
530 useGradient,
531 rangeGradient,
532 usePoint,
533 QStringLiteral(":/materials/ScatterMaterial"));
534 updateInstancedMaterialProperties(graphModel,
535 isHighlight: true,
536 seriesTexture: nullptr,
537 highlightTexture: graphModel->highlightTexture);
538 graphModel->selectionIndicator->setCastsShadows(!usePoint);
539 }
540
541 const DataItemHolder &dih = graphModel->instancing->dataArray().at(i: m_selectedItem);
542
543 graphModel->selectionIndicator->setPosition(dih.position);
544 graphModel->selectionIndicator->setRotation(dih.rotation);
545 graphModel->selectionIndicator->setScale(dih.scale);
546 graphModel->selectionIndicator->setVisible(true);
547 graphModel->instancing->hideDataItem(index: m_selectedItem);
548 updateItemLabel(position: graphModel->selectionIndicator->position());
549 graphModel->instancing->markDataDirty();
550 } else if ((m_selectedItem == -1 || m_selectedItemSeries != graphModel->series)
551 && graphModel->selectionIndicator) {
552 graphModel->selectionIndicator->setVisible(false);
553 }
554 }
555}
556
557void QQuickGraphsScatter::updateMaterialReference(ScatterModel *model)
558{
559 if (model->baseRef == nullptr) {
560 model->baseRef = createDataItem(series: model->series);
561 model->baseRef->setParent(model->series);
562 model->baseRef->setVisible(false);
563 }
564 if (model->selectionRef == nullptr) {
565 model->selectionRef = createDataItem(series: model->series);
566 model->selectionRef->setParent(model->series);
567 model->selectionRef->setVisible(false);
568 }
569
570 QQmlListReference baseRef(model->baseRef, "materials");
571 QQmlListReference selectionRef(model->selectionRef, "materials");
572
573 const QString &materialName = QStringLiteral(":/materials/ScatterMaterial");
574 if (!baseRef.size()) {
575 auto mat = createQmlCustomMaterial(fileName: materialName);
576 mat->setObjectName(materialName);
577 mat->setParent(model->baseRef);
578 baseRef.append(mat);
579 }
580 if (!selectionRef.size()) {
581 auto mat = createQmlCustomMaterial(fileName: materialName);
582 mat->setObjectName(materialName + QStringLiteral("_Selection"));
583 mat->setParent(model->selectionRef);
584 selectionRef.append(mat);
585 }
586}
587void QQuickGraphsScatter::updateItemMaterial(QQuick3DModel *item,
588 bool useGradient,
589 bool rangeGradient,
590 bool usePoint,
591 const QString &materialName)
592{
593 QQmlListReference materialsRef(item, "materials");
594 bool needNewMat = false;
595 if (!materialsRef.size()) {
596 needNewMat = true;
597 } else if (materialsRef.at(0)->objectName().contains(QStringLiteral("Instancing"))
598 != materialName.contains(QStringLiteral("Instancing"))) {
599 needNewMat = true;
600 }
601
602 if (needNewMat) {
603 materialsRef.clear();
604 auto newMaterial = createQmlCustomMaterial(fileName: materialName);
605 newMaterial->setObjectName(materialName);
606 newMaterial->setParent(item);
607 materialsRef.append(newMaterial);
608 }
609
610 auto material = qobject_cast<QQuick3DCustomMaterial *>(object: materialsRef.at(0));
611 if (!useGradient)
612 material->setProperty(name: "colorStyle", value: 0);
613 else if (!rangeGradient)
614 material->setProperty(name: "colorStyle", value: 1);
615 else
616 material->setProperty(name: "colorStyle", value: 2);
617
618 material->setProperty(name: "usePoint", value: usePoint);
619}
620
621void QQuickGraphsScatter::updateInstancedMaterialProperties(ScatterModel *graphModel,
622 const bool isHighlight,
623 QQuick3DTexture *seriesTexture,
624 QQuick3DTexture *highlightTexture,
625 const bool transparency)
626{
627 QQuick3DModel *model = nullptr;
628 if (isHighlight)
629 model = graphModel->selectionIndicator;
630 else
631 model = graphModel->instancingRootItem;
632
633 QQmlListReference materialsRef(model, "materials");
634
635 auto customMaterial = static_cast<QQuick3DCustomMaterial *>(materialsRef.at(0));
636 customMaterial->setProperty(name: "transparency", value: transparency);
637
638 QVariant textureInputAsVariant = customMaterial->property(name: "custex");
639 QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant
640 .value<QQuick3DShaderUtilsTextureInput *>();
641
642 if (isHighlight) {
643 textureInput->setTexture(highlightTexture);
644
645 if (selectedItemInSeries(series: graphModel->series))
646 m_selectedGradientPos = graphModel->instancing->customData().at(i: m_selectedItem);
647
648 customMaterial->setProperty(name: "gradientPos", value: m_selectedGradientPos);
649 } else {
650 textureInput->setTexture(seriesTexture);
651 }
652}
653
654void QQuickGraphsScatter::updateMaterialProperties(QQuick3DModel *item,
655 QQuick3DTexture *texture,
656 QColor color,
657 const bool transparency)
658{
659 QQmlListReference materialsRef(item, "materials");
660 auto customMaterial = static_cast<QQuick3DCustomMaterial *>(materialsRef.at(0));
661 customMaterial->setProperty(name: "transparency", value: transparency);
662
663 int style = customMaterial->property(name: "colorStyle").value<int>();
664 if (style == 0) {
665 customMaterial->setProperty(name: "uColor", value: color);
666 } else {
667 QVariant textureInputAsVariant = customMaterial->property(name: "custex");
668 QQuick3DShaderUtilsTextureInput *textureInput
669 = textureInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
670
671 textureInput->setTexture(texture);
672
673 const float scaleY = scaleWithBackground().y();
674 float rangeGradientYScaler = m_rangeGradientYHelper / scaleY;
675 float value = (item->y() + scaleY) * rangeGradientYScaler;
676 customMaterial->setProperty(name: "gradientPos", value);
677 }
678}
679
680QQuick3DTexture *QQuickGraphsScatter::createTexture()
681{
682 QQuick3DTexture *texture = new QQuick3DTexture();
683 texture->setParent(this);
684 texture->setRotationUV(-90.0f);
685 texture->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
686 texture->setVerticalTiling(QQuick3DTexture::ClampToEdge);
687 QQuickGraphsTextureData *textureData = new QQuickGraphsTextureData();
688 textureData->setParent(texture);
689 textureData->setParentItem(texture);
690 texture->setTextureData(textureData);
691
692 return texture;
693}
694
695QQuick3DNode *QQuickGraphsScatter::createSeriesRoot()
696{
697 auto model = new QQuick3DNode();
698
699 model->setParentItem(QQuick3DViewport::scene());
700 return model;
701}
702
703QQuick3DModel *QQuickGraphsScatter::createDataItem(QAbstract3DSeries *series)
704{
705 auto model = new QQuick3DModel();
706 model->setParent(this);
707 model->setParentItem(QQuick3DViewport::scene());
708 QString fileName = getMeshFileName(series);
709 if (fileName.isEmpty())
710 fileName = series->userDefinedMesh();
711
712 model->setSource(QUrl(fileName));
713 return model;
714}
715
716void QQuickGraphsScatter::removeDataItems(ScatterModel *graphModel,
717 QtGraphs3D::OptimizationHint optimizationHint)
718{
719 if (optimizationHint == QtGraphs3D::OptimizationHint::Default) {
720 delete graphModel->instancing;
721 graphModel->instancing = nullptr;
722 deleteDataItem(item: graphModel->instancingRootItem);
723 deleteDataItem(item: graphModel->selectionIndicator);
724 deleteDataItem(item: graphModel->baseRef);
725 deleteDataItem(item: graphModel->selectionRef);
726
727 graphModel->instancingRootItem = nullptr;
728 graphModel->selectionIndicator = nullptr;
729 graphModel->baseRef = nullptr;
730 graphModel->selectionRef = nullptr;
731 } else {
732 QList<QQuick3DModel *> &items = graphModel->dataItems;
733 removeDataItems(items, count: items.count());
734 }
735}
736
737void QQuickGraphsScatter::removeDataItems(QList<QQuick3DModel *> &items, qsizetype count)
738{
739 for (int i = 0; i < count; ++i) {
740 QQuick3DModel *item = items.takeLast();
741 QQmlListReference materialsRef(item, "materials");
742 if (materialsRef.size()) {
743 QObject *material = materialsRef.at(0);
744 delete material;
745 }
746 item->deleteLater();
747 }
748}
749
750QList<QScatter3DSeries *> QQuickGraphsScatter::scatterSeriesList()
751{
752 QList<QScatter3DSeries *> scatterSeriesList;
753 for (QAbstract3DSeries *abstractSeries : m_seriesList) {
754 QScatter3DSeries *scatterSeries = qobject_cast<QScatter3DSeries *>(object: abstractSeries);
755 if (scatterSeries)
756 scatterSeriesList.append(t: scatterSeries);
757 }
758
759 return scatterSeriesList;
760}
761
762void QQuickGraphsScatter::recreateDataItems()
763{
764 if (!isComponentComplete())
765 return;
766 QList<QScatter3DSeries *> seriesList = scatterSeriesList();
767 for (auto series : seriesList) {
768 for (const auto &model : std::as_const(t&: m_scatterGraphs)) {
769 if (model->series == series)
770 removeDataItems(graphModel: model, optimizationHint: optimizationHint());
771 }
772 }
773 markDataDirty();
774}
775
776void QQuickGraphsScatter::recreateDataItems(const QList<ScatterModel *> &graphs)
777{
778 if (!isComponentComplete())
779 return;
780 QList<QScatter3DSeries *> seriesList = scatterSeriesList();
781 for (auto series : seriesList) {
782 for (const auto &model : graphs) {
783 if (model->series == series)
784 removeDataItems(graphModel: model, optimizationHint: optimizationHint());
785 }
786 }
787 markDataDirty();
788}
789
790void QQuickGraphsScatter::addPointsToScatterModel(ScatterModel *graphModel, qsizetype count)
791{
792 for (int i = 0; i < count; i++) {
793 QQuick3DModel *item = createDataItem(series: graphModel->series);
794 item->setPickable(true);
795 item->setParent(graphModel->series);
796 graphModel->dataItems.push_back(t: item);
797 }
798 setSeriesVisualsDirty();
799}
800
801qsizetype QQuickGraphsScatter::sizeDifference(qsizetype size1, qsizetype size2)
802{
803 return size2 - size1;
804}
805
806QVector3D QQuickGraphsScatter::selectedItemPosition()
807{
808 QVector3D position;
809 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy)
810 position = {0.0f, 0.0f, 0.0f};
811 else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default)
812 position = {0.0f, 0.0f, 0.0f};
813
814 return position;
815}
816
817void QQuickGraphsScatter::fixMeshFileName(QString &fileName, QAbstract3DSeries *series)
818{
819 auto meshType = series->mesh();
820 // Should it be smooth?
821 if (series->isMeshSmooth() && meshType != QAbstract3DSeries::Mesh::Point
822 && meshType != QAbstract3DSeries::Mesh::UserDefined) {
823 fileName += QStringLiteral("Smooth");
824 }
825
826 // Should it be filled?
827 if (meshType != QAbstract3DSeries::Mesh::Sphere && meshType != QAbstract3DSeries::Mesh::Arrow
828 && meshType != QAbstract3DSeries::Mesh::Minimal
829 && meshType != QAbstract3DSeries::Mesh::Point
830 && meshType != QAbstract3DSeries::Mesh::UserDefined) {
831 fileName.append(QStringLiteral("Full"));
832 }
833}
834
835QString QQuickGraphsScatter::getMeshFileName(QAbstract3DSeries *series)
836{
837 QString fileName = {};
838 switch (series->mesh()) {
839 case QAbstract3DSeries::Mesh::Sphere:
840 fileName = QStringLiteral("defaultMeshes/sphereMesh");
841 break;
842 case QAbstract3DSeries::Mesh::Bar:
843 case QAbstract3DSeries::Mesh::Cube:
844 fileName = QStringLiteral("defaultMeshes/barMesh");
845 break;
846 case QAbstract3DSeries::Mesh::Pyramid:
847 fileName = QStringLiteral("defaultMeshes/pyramidMesh");
848 break;
849 case QAbstract3DSeries::Mesh::Cone:
850 fileName = QStringLiteral("defaultMeshes/coneMesh");
851 break;
852 case QAbstract3DSeries::Mesh::Cylinder:
853 fileName = QStringLiteral("defaultMeshes/cylinderMesh");
854 break;
855 case QAbstract3DSeries::Mesh::BevelBar:
856 case QAbstract3DSeries::Mesh::BevelCube:
857 fileName = QStringLiteral("defaultMeshes/bevelBarMesh");
858 break;
859 case QAbstract3DSeries::Mesh::Minimal:
860 fileName = QStringLiteral("defaultMeshes/minimalMesh");
861 break;
862 case QAbstract3DSeries::Mesh::Arrow:
863 fileName = QStringLiteral("defaultMeshes/arrowMesh");
864 break;
865 case QAbstract3DSeries::Mesh::Point:
866 fileName = shadowQuality() == QtGraphs3D::ShadowQuality::None
867 ? QStringLiteral("defaultMeshes/planeMesh")
868 : QStringLiteral("defaultMeshes/octagonMesh");
869 break;
870 case QAbstract3DSeries::Mesh::UserDefined:
871 break;
872 default:
873 fileName = QStringLiteral("defaultMeshes/sphereMesh");
874 }
875
876 fixMeshFileName(fileName, series);
877
878 return fileName;
879}
880
881void QQuickGraphsScatter::deleteDataItem(QQuick3DModel *item)
882{
883 if (item) {
884 QQmlListReference materialsRef(item, "materials");
885 if (materialsRef.size()) {
886 QObject *material = materialsRef.at(0);
887 delete material;
888 }
889 item->deleteLater();
890 item = nullptr;
891 }
892}
893
894void QQuickGraphsScatter::handleSeriesChanged(QList<QAbstract3DSeries *> changedSeries)
895{
896 Q_UNUSED(changedSeries)
897 // TODO: generate items and remove old items
898}
899
900bool QQuickGraphsScatter::selectedItemInSeries(const QScatter3DSeries *series)
901{
902 return (m_selectedItem != -1 && m_selectedItemSeries == series);
903}
904
905bool QQuickGraphsScatter::isDotPositionInAxisRange(QVector3D dotPos)
906{
907 return ((dotPos.x() >= axisX()->min() && dotPos.x() <= axisX()->max())
908 && (dotPos.y() >= axisY()->min() && dotPos.y() <= axisY()->max())
909 && (dotPos.z() >= axisZ()->min() && dotPos.z() <= axisZ()->max()));
910}
911
912QScatter3DSeries *QQuickGraphsScatter::selectedSeries() const
913{
914 return m_selectedItemSeries;
915}
916
917void QQuickGraphsScatter::setSelectedItem(qsizetype index, QScatter3DSeries *series)
918{
919 const QScatterDataProxy *proxy = 0;
920
921 // Series may already have been removed, so check it before setting the selection.
922 if (!m_seriesList.contains(t: series))
923 series = nullptr;
924
925 if (series)
926 proxy = series->dataProxy();
927
928 if (!proxy || index < 0 || index >= proxy->itemCount())
929 index = invalidSelectionIndex();
930
931 if (index != m_selectedItem || series != m_selectedItemSeries) {
932 bool seriesChanged = (series != m_selectedItemSeries);
933
934 // Clear hidden point from the previous selected series
935 if (seriesChanged) {
936 for (auto model : std::as_const(t&: m_scatterGraphs)) {
937 if (model->series && model->instancing && model->series == m_selectedItemSeries)
938 model->instancing->unhidePreviousDataItem();
939 }
940 }
941
942 m_selectedItem = index;
943 m_selectedItemSeries = series;
944 m_changeTracker.selectedItemChanged = true;
945
946 // Clear selection from other series and finally set new selection to the
947 // specified series
948 for (QAbstract3DSeries *otherSeries : m_seriesList) {
949 QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(otherSeries);
950 if (scatterSeries != m_selectedItemSeries)
951 scatterSeries->d_func()->setSelectedItem(invalidSelectionIndex());
952 }
953 if (m_selectedItemSeries)
954 m_selectedItemSeries->d_func()->setSelectedItem(m_selectedItem);
955
956 if (seriesChanged)
957 emit selectedSeriesChanged(series: m_selectedItemSeries);
958
959 emitNeedRender();
960 }
961
962 if (index != invalidSelectionIndex())
963 itemLabel()->setVisible(true);
964}
965
966void QQuickGraphsScatter::setSelectionMode(QtGraphs3D::SelectionFlags mode)
967{
968 // We only support single item selection mode and no selection mode
969 if (mode != QtGraphs3D::SelectionFlag::Item && mode != QtGraphs3D::SelectionFlag::None) {
970 qWarning(msg: "Unsupported selection mode - only none and item selection modes "
971 "are supported.");
972 return;
973 }
974
975 QQuickGraphsItem::setSelectionMode(mode);
976}
977
978void QQuickGraphsScatter::handleAxisAutoAdjustRangeChangedInOrientation(
979 QAbstract3DAxis::AxisOrientation orientation, bool autoAdjust)
980{
981 Q_UNUSED(orientation);
982 Q_UNUSED(autoAdjust);
983 adjustAxisRanges();
984}
985
986void QQuickGraphsScatter::handleAxisRangeChangedBySender(QObject *sender)
987{
988 QQuickGraphsItem::handleAxisRangeChangedBySender(sender);
989
990 m_isDataDirty = true;
991
992 // Update selected index - may be moved offscreen
993 setSelectedItem(index: m_selectedItem, series: m_selectedItemSeries);
994}
995
996QQmlListProperty<QScatter3DSeries> QQuickGraphsScatter::seriesList()
997{
998 return QQmlListProperty<QScatter3DSeries>(this,
999 this,
1000 &QQuickGraphsScatter::appendSeriesFunc,
1001 &QQuickGraphsScatter::countSeriesFunc,
1002 &QQuickGraphsScatter::atSeriesFunc,
1003 &QQuickGraphsScatter::clearSeriesFunc);
1004}
1005
1006void QQuickGraphsScatter::appendSeriesFunc(QQmlListProperty<QScatter3DSeries> *list,
1007 QScatter3DSeries *series)
1008{
1009 reinterpret_cast<QQuickGraphsScatter *>(list->data)->addSeries(series);
1010}
1011
1012qsizetype QQuickGraphsScatter::countSeriesFunc(QQmlListProperty<QScatter3DSeries> *list)
1013{
1014 return reinterpret_cast<QQuickGraphsScatter *>(list->data)->scatterSeriesList().size();
1015}
1016
1017QScatter3DSeries *QQuickGraphsScatter::atSeriesFunc(QQmlListProperty<QScatter3DSeries> *list,
1018 qsizetype index)
1019{
1020 return reinterpret_cast<QQuickGraphsScatter *>(list->data)->scatterSeriesList().at(i: index);
1021}
1022
1023void QQuickGraphsScatter::clearSeriesFunc(QQmlListProperty<QScatter3DSeries> *list)
1024{
1025 QQuickGraphsScatter *declScatter = reinterpret_cast<QQuickGraphsScatter *>(list->data);
1026 QList<QScatter3DSeries *> realList = declScatter->scatterSeriesList();
1027 qsizetype count = realList.size();
1028 for (int i = 0; i < count; i++)
1029 declScatter->removeSeries(series: realList.at(i));
1030}
1031
1032void QQuickGraphsScatter::addSeries(QScatter3DSeries *series)
1033{
1034 Q_ASSERT(series && series->type() == QAbstract3DSeries::SeriesType::Scatter);
1035
1036 QQuickGraphsItem::addSeriesInternal(series);
1037
1038 QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(series);
1039 if (scatterSeries->selectedItem() != invalidSelectionIndex())
1040 setSelectedItem(index: scatterSeries->selectedItem(), series: scatterSeries);
1041
1042 auto graphModel = new ScatterModel;
1043 graphModel->series = series;
1044 graphModel->seriesTexture = nullptr;
1045 graphModel->highlightTexture = nullptr;
1046 m_scatterGraphs.push_back(t: graphModel);
1047
1048 connectSeries(series);
1049
1050 if (series->selectedItem() != invalidSelectionIndex())
1051 setSelectedItem(index: series->selectedItem(), series);
1052}
1053
1054void QQuickGraphsScatter::removeSeries(QScatter3DSeries *series)
1055{
1056 bool wasVisible = (series && series->d_func()->m_graph == this && series->isVisible());
1057
1058 QQuickGraphsItem::removeSeriesInternal(series);
1059
1060 if (m_selectedItemSeries == series)
1061 setSelectedItem(index: invalidSelectionIndex(), series: 0);
1062
1063 if (wasVisible)
1064 adjustAxisRanges();
1065
1066 series->setParent(this); // Reparent as removing will leave series parentless
1067
1068 // Find scattergraph model
1069 for (QList<ScatterModel *>::ConstIterator it = m_scatterGraphs.cbegin();
1070 it != m_scatterGraphs.cend();) {
1071 if ((*it)->series == series) {
1072 removeDataItems(graphModel: *it, optimizationHint: optimizationHint());
1073
1074 if ((*it)->seriesTexture)
1075 delete (*it)->seriesTexture;
1076 if ((*it)->highlightTexture)
1077 delete (*it)->highlightTexture;
1078
1079 delete *it;
1080 it = m_scatterGraphs.erase(pos: it);
1081 } else {
1082 ++it;
1083 }
1084 }
1085
1086 disconnectSeries(series);
1087}
1088
1089void QQuickGraphsScatter::handleAxisXChanged(QAbstract3DAxis *axis)
1090{
1091 emit axisXChanged(axis: static_cast<QValue3DAxis *>(axis));
1092}
1093
1094void QQuickGraphsScatter::handleAxisYChanged(QAbstract3DAxis *axis)
1095{
1096 emit axisYChanged(axis: static_cast<QValue3DAxis *>(axis));
1097}
1098
1099void QQuickGraphsScatter::handleAxisZChanged(QAbstract3DAxis *axis)
1100{
1101 emit axisZChanged(axis: static_cast<QValue3DAxis *>(axis));
1102}
1103
1104void QQuickGraphsScatter::handleSeriesMeshChanged()
1105{
1106 recreateDataItems();
1107}
1108
1109void QQuickGraphsScatter::handleMeshSmoothChanged(bool enable)
1110{
1111 Q_UNUSED(enable);
1112 QScatter3DSeries *series = qobject_cast<QScatter3DSeries *>(object: sender());
1113 for (auto &model : std::as_const(t&: m_scatterGraphs)) {
1114 if (model->series == series)
1115 removeDataItems(graphModel: model, optimizationHint: optimizationHint());
1116 }
1117 markDataDirty();
1118}
1119
1120void QQuickGraphsScatter::handleArrayReset()
1121{
1122 QScatter3DSeries *series;
1123 if (qobject_cast<QScatterDataProxy *>(object: sender()))
1124 series = static_cast<QScatterDataProxy *>(sender())->series();
1125 else
1126 series = static_cast<QScatter3DSeries *>(sender());
1127
1128 if (series->isVisible()) {
1129 adjustAxisRanges();
1130 m_isDataDirty = true;
1131 }
1132 if (!m_changedSeriesList.contains(t: series))
1133 m_changedSeriesList.append(t: series);
1134 setSelectedItem(index: m_selectedItem, series: m_selectedItemSeries);
1135 series->d_func()->markItemLabelDirty();
1136 emitNeedRender();
1137}
1138
1139void QQuickGraphsScatter::handleItemsAdded(qsizetype startIndex, qsizetype count)
1140{
1141 Q_UNUSED(startIndex);
1142 Q_UNUSED(count);
1143 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
1144 if (series->isVisible()) {
1145 adjustAxisRanges();
1146 m_isDataDirty = true;
1147 }
1148 if (!m_changedSeriesList.contains(t: series))
1149 m_changedSeriesList.append(t: series);
1150 emitNeedRender();
1151}
1152
1153void QQuickGraphsScatter::handleItemsChanged(qsizetype startIndex, qsizetype count)
1154{
1155 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
1156 qsizetype oldChangeCount = m_changedItems.size();
1157 if (!oldChangeCount)
1158 m_changedItems.reserve(asize: count);
1159
1160 for (int i = 0; i < count; i++) {
1161 bool newItem = true;
1162 qsizetype candidate = startIndex + i;
1163 for (qsizetype j = 0; j < oldChangeCount; j++) {
1164 const ChangeItem &oldChangeItem = m_changedItems.at(i: j);
1165 if (oldChangeItem.index == candidate && series == oldChangeItem.series) {
1166 newItem = false;
1167 break;
1168 }
1169 }
1170 if (newItem) {
1171 ChangeItem newChangeItem = {.series: series, .index: candidate};
1172 m_changedItems.append(t: newChangeItem);
1173 if (series == m_selectedItemSeries && m_selectedItem == candidate)
1174 series->d_func()->markItemLabelDirty();
1175 }
1176 }
1177
1178 if (count) {
1179 m_changeTracker.itemChanged = true;
1180 if (series->isVisible())
1181 adjustAxisRanges();
1182 emitNeedRender();
1183 }
1184}
1185
1186void QQuickGraphsScatter::handleItemsRemoved(qsizetype startIndex, qsizetype count)
1187{
1188 Q_UNUSED(startIndex);
1189 Q_UNUSED(count);
1190 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
1191 if (series == m_selectedItemSeries) {
1192 // If items removed from selected series before the selection, adjust the selection
1193 qsizetype selectedItem = m_selectedItem;
1194 if (startIndex <= selectedItem) {
1195 if ((startIndex + count) > selectedItem)
1196 selectedItem = -1; // Selected item removed
1197 else
1198 selectedItem -= count; // Move selected item down by amount of item removed
1199
1200 setSelectedItem(index: selectedItem, series: m_selectedItemSeries);
1201 }
1202 }
1203
1204 if (series->isVisible()) {
1205 adjustAxisRanges();
1206 m_isDataDirty = true;
1207 }
1208 if (!m_changedSeriesList.contains(t: series))
1209 m_changedSeriesList.append(t: series);
1210
1211 if (m_recordInsertsAndRemoves) {
1212 InsertRemoveRecord record(false, startIndex, count, series);
1213 m_insertRemoveRecords.append(t: record);
1214 }
1215
1216 emitNeedRender();
1217}
1218
1219void QQuickGraphsScatter::adjustAxisRanges()
1220{
1221 QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
1222 QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
1223 QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
1224 bool adjustX = (valueAxisX && valueAxisX->isAutoAdjustRange());
1225 bool adjustY = (valueAxisY && valueAxisY->isAutoAdjustRange());
1226 bool adjustZ = (valueAxisZ && valueAxisZ->isAutoAdjustRange());
1227
1228 if (adjustX || adjustY || adjustZ) {
1229 float minValueX = 0.0f;
1230 float maxValueX = 0.0f;
1231 float minValueY = 0.0f;
1232 float maxValueY = 0.0f;
1233 float minValueZ = 0.0f;
1234 float maxValueZ = 0.0f;
1235 qsizetype seriesCount = m_seriesList.size();
1236 for (int series = 0; series < seriesCount; series++) {
1237 const QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(
1238 m_seriesList.at(i: series));
1239 const QScatterDataProxy *proxy = scatterSeries->dataProxy();
1240 if (scatterSeries->isVisible() && proxy) {
1241 QVector3D minLimits;
1242 QVector3D maxLimits;
1243 proxy->d_func()->limitValues(minValues&: minLimits,
1244 maxValues&: maxLimits,
1245 axisX: valueAxisX,
1246 axisY: valueAxisY,
1247 axisZ: valueAxisZ);
1248 if (adjustX) {
1249 if (!series) {
1250 // First series initializes the values
1251 minValueX = minLimits.x();
1252 maxValueX = maxLimits.x();
1253 } else {
1254 minValueX = qMin(a: minValueX, b: minLimits.x());
1255 maxValueX = qMax(a: maxValueX, b: maxLimits.x());
1256 }
1257 }
1258 if (adjustY) {
1259 if (!series) {
1260 // First series initializes the values
1261 minValueY = minLimits.y();
1262 maxValueY = maxLimits.y();
1263 } else {
1264 minValueY = qMin(a: minValueY, b: minLimits.y());
1265 maxValueY = qMax(a: maxValueY, b: maxLimits.y());
1266 }
1267 }
1268 if (adjustZ) {
1269 if (!series) {
1270 // First series initializes the values
1271 minValueZ = minLimits.z();
1272 maxValueZ = maxLimits.z();
1273 } else {
1274 minValueZ = qMin(a: minValueZ, b: minLimits.z());
1275 maxValueZ = qMax(a: maxValueZ, b: maxLimits.z());
1276 }
1277 }
1278 }
1279 }
1280
1281 static const float adjustmentRatio = 20.0f;
1282 static const float defaultAdjustment = 1.0f;
1283
1284 if (adjustX) {
1285 // If all points at same coordinate, need to default to some valid range
1286 float adjustment = 0.0f;
1287 if (minValueX == maxValueX) {
1288 if (adjustZ) {
1289 // X and Z are linked to have similar unit size, so choose the valid
1290 // range based on it
1291 if (minValueZ == maxValueZ)
1292 adjustment = defaultAdjustment;
1293 else
1294 adjustment = qAbs(t: maxValueZ - minValueZ) / adjustmentRatio;
1295 } else {
1296 if (valueAxisZ)
1297 adjustment = qAbs(t: valueAxisZ->max() - valueAxisZ->min()) / adjustmentRatio;
1298 else
1299 adjustment = defaultAdjustment;
1300 }
1301 }
1302 valueAxisX->d_func()->setRange(min: minValueX - adjustment, max: maxValueX + adjustment, suppressWarnings: true);
1303 }
1304 if (adjustY) {
1305 // If all points at same coordinate, need to default to some valid range
1306 // Y-axis unit is not dependent on other axes, so simply adjust +-1.0f
1307 float adjustment = 0.0f;
1308 if (minValueY == maxValueY)
1309 adjustment = defaultAdjustment;
1310 valueAxisY->d_func()->setRange(min: minValueY - adjustment, max: maxValueY + adjustment, suppressWarnings: true);
1311 }
1312 if (adjustZ) {
1313 // If all points at same coordinate, need to default to some valid range
1314 float adjustment = 0.0f;
1315 if (minValueZ == maxValueZ) {
1316 if (adjustX) {
1317 // X and Z are linked to have similar unit size, so choose the valid
1318 // range based on it
1319 if (minValueX == maxValueX)
1320 adjustment = defaultAdjustment;
1321 else
1322 adjustment = qAbs(t: maxValueX - minValueX) / adjustmentRatio;
1323 } else {
1324 if (valueAxisX)
1325 adjustment = qAbs(t: valueAxisX->max() - valueAxisX->min()) / adjustmentRatio;
1326 else
1327 adjustment = defaultAdjustment;
1328 }
1329 }
1330 valueAxisZ->d_func()->setRange(min: minValueZ - adjustment, max: maxValueZ + adjustment, suppressWarnings: true);
1331 }
1332 }
1333}
1334
1335void QQuickGraphsScatter::handleItemsInserted(qsizetype startIndex, qsizetype count)
1336{
1337 Q_UNUSED(startIndex);
1338 Q_UNUSED(count);
1339 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
1340 if (series == m_selectedItemSeries) {
1341 // If items inserted to selected series before the selection, adjust the selection
1342 qsizetype selectedItem = m_selectedItem;
1343 if (startIndex <= selectedItem) {
1344 selectedItem += count;
1345 setSelectedItem(index: selectedItem, series: m_selectedItemSeries);
1346 }
1347 }
1348
1349 if (series->isVisible()) {
1350 adjustAxisRanges();
1351 m_isDataDirty = true;
1352 }
1353 if (!m_changedSeriesList.contains(t: series))
1354 m_changedSeriesList.append(t: series);
1355
1356 if (m_recordInsertsAndRemoves) {
1357 InsertRemoveRecord record(true, startIndex, count, series);
1358 m_insertRemoveRecords.append(t: record);
1359 }
1360
1361 emitNeedRender();
1362}
1363
1364bool QQuickGraphsScatter::doPicking(QPointF position)
1365{
1366 if (!QQuickGraphsItem::doPicking(point: position))
1367 return false;
1368
1369 if (selectionMode() == QtGraphs3D::SelectionFlag::Item) {
1370 QList<QQuick3DPickResult> results = pickAll(x: position.x(), y: position.y());
1371 if (!results.empty()) {
1372 for (const auto &result : std::as_const(t&: results)) {
1373 if (const auto &hitItem = result.objectHit()) {
1374 if (hitItem == backgroundBB() || hitItem == background()) {
1375 m_clickedType = QtGraphs3D::ElementType::None;
1376 clearSelectionModel();
1377 continue;
1378 }
1379
1380 if (m_clickedType != QtGraphs3D::ElementType::AxisXLabel
1381 && m_clickedType != QtGraphs3D::ElementType::AxisYLabel
1382 && m_clickedType != QtGraphs3D::ElementType::AxisZLabel) {
1383 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) {
1384 setSelected(hitItem);
1385 handleSelectedElementChange(type: QtGraphs3D::ElementType::Series);
1386 break;
1387 } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) {
1388 setSelected(root: hitItem, index: result.instanceIndex());
1389 handleSelectedElementChange(type: QtGraphs3D::ElementType::Series);
1390 break;
1391 }
1392 } else {
1393 clearSelectionModel();
1394 }
1395 }
1396 }
1397 } else {
1398 clearSelectionModel();
1399 handleSelectedElementChange(type: QtGraphs3D::ElementType::None);
1400 }
1401 }
1402
1403 return true;
1404}
1405
1406void QQuickGraphsScatter::updateShadowQuality(QtGraphs3D::ShadowQuality quality)
1407{
1408 // Were shadows visible before?
1409 bool prevShadowsVisible = light()->castsShadow();
1410 QQuickGraphsItem::updateShadowQuality(quality);
1411 setSeriesVisualsDirty();
1412
1413 if (prevShadowsVisible != light()->castsShadow()) {
1414 // Need to change mesh for series using point type
1415 QList<ScatterModel *> graphs;
1416 for (const auto &graph : std::as_const(t&: m_scatterGraphs)) {
1417 if (graph->series->mesh() == QAbstract3DSeries::Mesh::Point)
1418 graphs.append(t: graph);
1419 }
1420 recreateDataItems(graphs);
1421 }
1422}
1423
1424void QQuickGraphsScatter::updateLightStrength()
1425{
1426 for (auto graphModel : m_scatterGraphs) {
1427 for (const auto &obj : std::as_const(t&: graphModel->dataItems)) {
1428 QQmlListReference materialsRef(obj, "materials");
1429 auto material = qobject_cast<QQuick3DCustomMaterial *>(object: materialsRef.at(0));
1430 material->setProperty(name: "specularBrightness", value: lightStrength() * 0.05);
1431 }
1432 }
1433}
1434
1435void QQuickGraphsScatter::startRecordingRemovesAndInserts()
1436{
1437 m_recordInsertsAndRemoves = false;
1438
1439 if (m_scene->selectionQueryPosition() != m_scene->invalidSelectionPoint()) {
1440 m_recordInsertsAndRemoves = true;
1441 if (m_insertRemoveRecords.size()) {
1442 m_insertRemoveRecords.clear();
1443 // Reserve some space for remove/insert records to avoid unnecessary reallocations.
1444 m_insertRemoveRecords.reserve(asize: insertRemoveRecordReserveSize);
1445 }
1446 }
1447}
1448
1449void QQuickGraphsScatter::componentComplete()
1450{
1451 QQuickGraphsItem::componentComplete();
1452 QObject::connect(sender: cameraTarget(),
1453 signal: &QQuick3DNode::rotationChanged,
1454 context: this,
1455 slot: &QQuickGraphsScatter::cameraRotationChanged);
1456
1457 graphsInputHandler()->setGraphsItem(this);
1458}
1459
1460void QQuickGraphsScatter::connectSeries(QScatter3DSeries *series)
1461{
1462 QObject::connect(sender: series,
1463 signal: &QScatter3DSeries::meshChanged,
1464 context: this,
1465 slot: &QQuickGraphsScatter::handleSeriesMeshChanged);
1466 QObject::connect(sender: series,
1467 signal: &QScatter3DSeries::meshSmoothChanged,
1468 context: this,
1469 slot: &QQuickGraphsScatter::handleMeshSmoothChanged);
1470 QObject::connect(sender: series,
1471 signal: &QScatter3DSeries::itemSizeChanged,
1472 context: this,
1473 slot: &QQuickGraphsScatter::markDataDirty);
1474}
1475
1476void QQuickGraphsScatter::calculateSceneScalingFactors()
1477{
1478 float marginV = 0.0f;
1479 float marginH = 0.0f;
1480 if (margin() < 0.0f) {
1481 if (m_maxItemSize > m_defaultMaxSize)
1482 marginH = m_maxItemSize / m_itemScaler;
1483 else
1484 marginH = m_defaultMaxSize;
1485 marginV = marginH;
1486 } else {
1487 marginH = margin();
1488 marginV = margin();
1489 }
1490 if (isPolar()) {
1491 float polarMargin = calculatePolarBackgroundMargin();
1492 marginH = qMax(a: marginH, b: polarMargin);
1493 }
1494
1495 float tHorizontalAspectRatio;
1496 if (isPolar())
1497 tHorizontalAspectRatio = 1.0f;
1498 else
1499 tHorizontalAspectRatio = horizontalAspectRatio();
1500
1501 QSizeF areaSize;
1502 if (qFuzzyIsNull(f: tHorizontalAspectRatio)) {
1503 areaSize.setHeight(axisZ()->max() - axisZ()->min());
1504 areaSize.setWidth(axisX()->max() - axisX()->min());
1505 } else {
1506 areaSize.setHeight(1.0f);
1507 areaSize.setWidth(tHorizontalAspectRatio);
1508 }
1509
1510 float horizontalMaxDimension;
1511 float scaleY = 0.0f;
1512 if (aspectRatio() > 2.0f) {
1513 horizontalMaxDimension = 2.0f;
1514 scaleY = 2.0f / aspectRatio();
1515 } else {
1516 horizontalMaxDimension = aspectRatio();
1517 scaleY = 1.0f;
1518 }
1519
1520 if (isPolar())
1521 m_polarRadius = horizontalMaxDimension;
1522
1523 float scaleFactor = qMax(a: areaSize.width(), b: areaSize.height());
1524 float scaleX = horizontalMaxDimension * areaSize.width() / scaleFactor;
1525 float scaleZ = horizontalMaxDimension * areaSize.height() / scaleFactor;
1526
1527 setBackgroundScaleMargin({marginH, marginV, marginH});
1528
1529 setScaleWithBackground({scaleX, scaleY, scaleZ});
1530 setScale({scaleX * 2.0f, scaleY * 2.0f, scaleZ * -2.0f});
1531 setTranslate({-scaleX, -scaleY, scaleZ});
1532}
1533
1534float QQuickGraphsScatter::calculatePointScaleSize()
1535{
1536 QList<QScatter3DSeries *> series = scatterSeriesList();
1537 int totalDataSize = 0;
1538 for (const auto &scatterSeries : std::as_const(t&: series)) {
1539 if (scatterSeries->isVisible())
1540 totalDataSize += scatterSeries->dataArray().size();
1541 }
1542
1543 return qBound(min: m_defaultMinSize, val: 2.0f / float(qSqrt(v: qreal(totalDataSize))), max: m_defaultMaxSize);
1544}
1545
1546void QQuickGraphsScatter::updatePointScaleSize()
1547{
1548 m_pointScale = calculatePointScaleSize();
1549}
1550
1551void QQuickGraphsScatter::calculatePolarXZ(const float posX,
1552 const float posZ,
1553 float &x,
1554 float &z) const
1555{
1556 const qreal angle = posX * (M_PI * 2.0f);
1557 const qreal radius = posZ;
1558
1559 x = static_cast<float>(radius * qSin(v: angle)) * m_polarRadius;
1560 z = -static_cast<float>(radius * qCos(v: angle)) * m_polarRadius;
1561}
1562
1563QQuick3DModel *QQuickGraphsScatter::selected() const
1564{
1565 return m_selected;
1566}
1567
1568void QQuickGraphsScatter::setSelected(QQuick3DModel *newSelected)
1569{
1570 if (newSelected != m_selected) {
1571 m_previousSelected = m_selected;
1572 m_selected = newSelected;
1573
1574 auto series = static_cast<QScatter3DSeries *>(m_selected->parent());
1575
1576 // Find scattermodel
1577 ScatterModel *graphModel = nullptr;
1578
1579 for (const auto &model : std::as_const(t&: m_scatterGraphs)) {
1580 if (model->series == series) {
1581 graphModel = model;
1582 break;
1583 }
1584 }
1585
1586 if (graphModel) {
1587 qsizetype index = graphModel->dataItems.indexOf(t: m_selected);
1588 setSelectedItem(index, series);
1589 setSeriesVisualsDirty();
1590 setSelectedItemChanged(true);
1591 }
1592 }
1593}
1594
1595void QQuickGraphsScatter::setSelected(QQuick3DModel *root, qsizetype index)
1596{
1597 auto series = static_cast<QScatter3DSeries *>(root->parent());
1598 if (index != m_selectedItem || series != m_selectedItemSeries) {
1599 setSeriesVisualsDirty();
1600 setSelectedItem(index, series);
1601 setSelectedItemChanged(true);
1602 }
1603}
1604
1605void QQuickGraphsScatter::clearSelectionModel()
1606{
1607 if (optimizationHint() == QtGraphs3D::OptimizationHint::Default)
1608 clearAllSelectionInstanced();
1609
1610 setSelectedItem(index: invalidSelectionIndex(), series: nullptr);
1611
1612 if (itemLabel())
1613 itemLabel()->setVisible(false);
1614
1615 setSeriesVisualsDirty();
1616 m_selected = nullptr;
1617 m_previousSelected = nullptr;
1618}
1619
1620void QQuickGraphsScatter::clearAllSelectionInstanced()
1621{
1622 for (const auto &graph : m_scatterGraphs) {
1623 if (graph->instancing)
1624 graph->instancing->resetVisibilty();
1625 }
1626}
1627
1628void QQuickGraphsScatter::optimizationChanged(QtGraphs3D::OptimizationHint toOptimization)
1629{
1630 if (toOptimization == QtGraphs3D::OptimizationHint::Default) {
1631 for (const auto &graph : std::as_const(t&: m_scatterGraphs))
1632 removeDataItems(graphModel: graph, optimizationHint: QtGraphs3D::OptimizationHint::Legacy);
1633 } else {
1634 for (const auto &graph : std::as_const(t&: m_scatterGraphs))
1635 removeDataItems(graphModel: graph, optimizationHint: QtGraphs3D::OptimizationHint::Default);
1636 }
1637 setSeriesVisualsDirty();
1638}
1639
1640void QQuickGraphsScatter::updateGraph()
1641{
1642 updatePointScaleSize();
1643 if (m_optimizationChanged) {
1644 optimizationChanged(toOptimization: optimizationHint());
1645 m_optimizationChanged = false;
1646 }
1647
1648 for (auto graphModel : std::as_const(t&: m_scatterGraphs)) {
1649 bool seriesVisible = graphModel->series->isVisible();
1650 if (isDataDirty()) {
1651 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy && seriesVisible) {
1652 if (graphModel->dataItems.count() != graphModel->series->dataProxy()->itemCount()) {
1653 qsizetype sizeDiff = sizeDifference(size1: graphModel->dataItems.count(),
1654 size2: graphModel->series->dataProxy()->itemCount());
1655
1656 if (sizeDiff > 0)
1657 addPointsToScatterModel(graphModel, count: sizeDiff);
1658 else
1659 removeDataItems(items&: graphModel->dataItems, count: qAbs(t: sizeDiff));
1660 }
1661 } else if (optimizationHint() == QtGraphs3D::OptimizationHint::Default
1662 && seriesVisible) {
1663 if (graphModel->instancing == nullptr) {
1664 graphModel->instancing = new ScatterInstancing;
1665 graphModel->instancing->setParent(graphModel->series);
1666 }
1667 if (graphModel->instancingRootItem == nullptr) {
1668 graphModel->instancingRootItem = createDataItem(series: graphModel->series);
1669 graphModel->instancingRootItem->setParent(graphModel->series);
1670 graphModel->instancingRootItem->setInstancing(graphModel->instancing);
1671 if (selectionMode() != QtGraphs3D::SelectionFlag::None) {
1672 graphModel->instancingRootItem->setPickable(true);
1673 graphModel->selectionIndicator = createDataItem(series: graphModel->series);
1674 graphModel->selectionIndicator->setVisible(false);
1675 }
1676 }
1677 }
1678 }
1679
1680 if (seriesVisible && (isDataDirty() || isSeriesVisualsDirty()))
1681 updateScatterGraphItemPositions(graphModel);
1682
1683 if (seriesVisible
1684 && (isSeriesVisualsDirty()
1685 || (graphModel->instancing && graphModel->instancing->isDirty()))) {
1686 updateScatterGraphItemVisuals(graphModel);
1687 }
1688
1689 const bool validSelection = (m_selectedItemSeries == graphModel->series
1690 && m_selectedItem != invalidSelectionIndex())
1691 && selectedItemInRange(graphModel);
1692
1693 if (validSelection) {
1694 QVector3D selectionPosition = {0.0f, 0.0f, 0.0f};
1695 if (optimizationHint() == QtGraphs3D::OptimizationHint::Legacy) {
1696 QQuick3DModel *selectedModel = graphModel->dataItems.at(i: m_selectedItem);
1697
1698 selectionPosition = selectedModel->position();
1699 } else {
1700 selectionPosition = graphModel->instancing->dataArray().at(i: m_selectedItem).position;
1701 }
1702
1703 updateItemLabel(position: selectionPosition);
1704 QString label = m_selectedItemSeries->itemLabel();
1705 itemLabel()->setProperty(name: "labelText", value: label);
1706 if (!label.compare(s: hiddenLabelTag))
1707 itemLabel()->setVisible(false);
1708 }
1709 }
1710
1711 if (m_selectedItem == invalidSelectionIndex())
1712 itemLabel()->setVisible(false);
1713
1714 setItemSelected(m_selectedItem != invalidSelectionIndex());
1715}
1716
1717void QQuickGraphsScatter::synchData()
1718{
1719 QList<QScatter3DSeries *> seriesList = scatterSeriesList();
1720
1721 float maxItemSize = 0.0f;
1722 for (const auto &series : std::as_const(t&: seriesList)) {
1723 if (series->isVisible()) {
1724 float itemSize = series->itemSize();
1725 if (itemSize > maxItemSize)
1726 maxItemSize = itemSize;
1727 }
1728 }
1729
1730 m_maxItemSize = maxItemSize;
1731
1732 updatePointScaleSize();
1733 QQuickGraphsItem::synchData();
1734 setMinCameraYRotation(-90.0f);
1735
1736 m_pointScale = calculatePointScaleSize();
1737
1738 if (hasSelectedItemChanged()) {
1739 if (m_selectedItem != invalidSelectionIndex()) {
1740 QString itemLabelText = m_selectedItemSeries->itemLabel();
1741 itemLabel()->setProperty(name: "labelText", value: itemLabelText);
1742 if (!itemLabelText.compare(s: hiddenLabelTag))
1743 itemLabel()->setVisible(false);
1744 }
1745 setSelectedItemChanged(false);
1746 }
1747}
1748
1749void QQuickGraphsScatter::cameraRotationChanged()
1750{
1751 m_isDataDirty = true;
1752}
1753
1754void QQuickGraphsScatter::handleOptimizationHintChange(QtGraphs3D::OptimizationHint hint)
1755{
1756 Q_UNUSED(hint)
1757 m_optimizationChanged = true;
1758}
1759
1760void QQuickGraphsScatter::handleSeriesVisibilityChangedBySender(QObject *sender)
1761{
1762 QQuickGraphsItem::handleSeriesVisibilityChangedBySender(sender);
1763 auto series = static_cast<QScatter3DSeries *>(sender);
1764 if (optimizationHint() == QtGraphs3D::OptimizationHint::Default) {
1765 ScatterModel *graphModel = findGraphModel(series);
1766 if (graphModel && graphModel->series == series) {
1767 if (graphModel->instancingRootItem)
1768 graphModel->instancingRootItem->setVisible(series->isVisible());
1769 if (series == m_selectedItemSeries) {
1770 itemLabel()->setVisible(series->isVisible());
1771 graphModel->selectionIndicator->setVisible(series->isVisible());
1772 }
1773 }
1774 }
1775}
1776
1777bool QQuickGraphsScatter::selectedItemInRange(const ScatterModel *graphModel)
1778{
1779 qsizetype itemCount;
1780 if (optimizationHint() == QtGraphs3D::OptimizationHint::Default)
1781 itemCount = graphModel->instancing->dataArray().count();
1782 else
1783 itemCount = graphModel->dataItems.count();
1784
1785 return m_selectedItem >= 0 && m_selectedItem < itemCount;
1786}
1787
1788QQuickGraphsScatter::ScatterModel *QQuickGraphsScatter::findGraphModel(QScatter3DSeries *series)
1789{
1790 for (const auto &graphModel : std::as_const(t&: m_scatterGraphs)) {
1791 if (graphModel->series == series)
1792 return graphModel;
1793 }
1794 return nullptr;
1795}
1796QT_END_NAMESPACE
1797

source code of qtgraphs/src/graphs3d/qml/qquickgraphsscatter.cpp