1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <QtCore/qfileinfo.h>
5#include "qcustom3ditem_p.h"
6#include "qgraphs3dlogging_p.h"
7
8QT_BEGIN_NAMESPACE
9
10/*!
11 * \class QCustom3DItem
12 * \inmodule QtGraphs
13 * \ingroup graphs_3D
14 * \brief The QCustom3DItem class adds a custom item to a graph.
15 *
16 * A custom item has a custom mesh, position, scaling, rotation, and an optional
17 * texture.
18 *
19 * \sa Q3DGraphsWidgetItem::addCustomItem()
20 */
21
22/*!
23 * \qmltype Custom3DItem
24 * \inqmlmodule QtGraphs
25 * \ingroup graphs_qml_3D
26 * \nativetype QCustom3DItem
27 * \brief Adds a custom item to a graph.
28 *
29 * A custom item has a custom mesh, position, scaling, rotation, and an optional
30 * texture.
31 */
32
33/*! \qmlproperty string Custom3DItem::meshFile
34 *
35 * The item mesh file name. The item in the file must be mesh format.
36 * The mesh files are recommended to include vertices, normals, and UVs.
37 */
38
39/*! \qmlproperty string Custom3DItem::textureFile
40 *
41 * The texture file name for the item. If left unset, a solid gray texture will
42 * be used.
43 *
44 * \note To conserve memory, the QImage loaded from the file is cleared after a
45 * texture is created.
46 */
47
48/*! \qmlproperty vector3d Custom3DItem::position
49 *
50 * The item position as a \l [QtQuick] vector3d. Defaults to \c {vector3d(0.0,
51 * 0.0, 0.0)}.
52 *
53 * Item position is specified either in data coordinates or in absolute
54 * coordinates, depending on the value of the positionAbsolute property. When
55 * using absolute coordinates, values between \c{-1.0...1.0} are
56 * within axis ranges.
57 *
58 * \note Items positioned outside any axis range are not rendered if
59 * positionAbsolute is \c{false}, unless the item is a Custom3DVolume that would
60 * be partially visible and scalingAbsolute is also \c{false}. In that case, the
61 * visible portion of the volume will be rendered.
62 *
63 * \sa positionAbsolute, scalingAbsolute
64 */
65
66/*! \qmlproperty bool Custom3DItem::positionAbsolute
67 *
68 * Defines whether item position is to be handled in data coordinates or in
69 * absolute coordinates. Defaults to \c{false}. Items with absolute coordinates
70 * will always be rendered, whereas items with data coordinates are only
71 * rendered if they are within axis ranges.
72 *
73 * \sa position
74 */
75
76/*! \qmlproperty vector3d Custom3DItem::scaling
77 *
78 * The item scaling as a \l [QtQuick] vector3d type. Defaults to
79 * \c {vector3d(0.1, 0.1, 0.1)}.
80 *
81 * Item scaling is specified either in data values or in absolute values,
82 * depending on the value of the scalingAbsolute property. The default vector
83 * interpreted as absolute values sets the item to
84 * 10% of the height of the graph, provided the item mesh is normalized and the
85 * graph aspect ratios have not been changed from the defaults.
86 *
87 * \note Only absolute scaling is supported for Custom3DLabel items or for
88 * custom items used in \l{GraphsItem3D::polar}{polar} graphs.
89 *
90 * \note In Qt 6.8 models were incorrectly assumed to be scaled to a size of 1 (-0.5...0.5)
91 * by default, when they in reality are scaled to the size of 2 (-1...1). Because of this, all
92 * custom items from Qt 6.9 onwards are twice the size compared to Qt 6.8
93 *
94 * \sa scalingAbsolute
95 */
96
97/*! \qmlproperty bool Custom3DItem::scalingAbsolute
98 *
99 * Defines whether item scaling is to be handled in data values or in absolute
100 * values. Defaults to \c{true}. Items with absolute scaling will be rendered at
101 * the same size, regardless of axis ranges. Items with data scaling will change
102 * their apparent size according to the axis ranges. If positionAbsolute is
103 * \c{true}, this property is ignored and scaling is interpreted as an absolute
104 * value. If the item has rotation, the data scaling is calculated on the
105 * unrotated item. Similarly, for Custom3DVolume items, the range clipping is
106 * calculated on the unrotated item.
107 *
108 * \note Only absolute scaling is supported for Custom3DLabel items or for
109 * custom items used in \l{GraphsItem3D::polar}{polar} graphs.
110 *
111 * \note The custom item's mesh must be normalized to the range \c{[-1 ,1]}, or
112 * the data scaling will not be accurate.
113 *
114 * \sa scaling, positionAbsolute
115 */
116
117/*! \qmlproperty quaternion Custom3DItem::rotation
118 *
119 * The item rotation as a \l [QtQuick] quaternion. Defaults to
120 * \c {quaternion(0.0, 0.0, 0.0, 0.0)}.
121 */
122
123/*! \qmlproperty bool Custom3DItem::visible
124 *
125 * The visibility of the item. Defaults to \c{true}.
126 */
127
128/*! \qmlproperty bool Custom3DItem::shadowCasting
129 *
130 * Defines whether shadow casting for the item is enabled. Defaults to \c{true}.
131 * If \c{false}, the item does not cast shadows regardless of
132 * \l{QtGraphs3D::ShadowQuality}{ShadowQuality}.
133 */
134
135/*!
136 * \qmlmethod void Custom3DItem::setRotationAxisAndAngle(vector3d axis, real
137 * angle)
138 *
139 * A convenience function to construct the rotation quaternion from \a axis and
140 * \a angle.
141 *
142 * \sa rotation
143 */
144
145/*!
146 \qmlsignal Custom3DItem::meshFileChanged(string meshFile)
147
148 This signal is emitted when meshFile changes to \a meshFile.
149*/
150/*!
151 \qmlsignal Custom3DItem::textureFileChanged(string textureFile)
152
153 This signal is emitted when textureFile changes to \a textureFile.
154*/
155/*!
156 \qmlsignal Custom3DItem::positionChanged(vector3d position)
157
158 This signal is emitted when item \l position changes to \a position.
159*/
160/*!
161 \qmlsignal Custom3DItem::positionAbsoluteChanged(bool positionAbsolute)
162
163 This signal is emitted when positionAbsolute changes to \a positionAbsolute.
164*/
165/*!
166 \qmlsignal Custom3DItem::scalingChanged(vector3d scaling)
167
168 This signal is emitted when \l scaling changes to \a scaling.
169*/
170/*!
171 \qmlsignal Custom3DItem::rotationChanged(quaternion rotation)
172
173 This signal is emitted when \l rotation changes to \a rotation.
174*/
175/*!
176 \qmlsignal Custom3DItem::visibleChanged(bool visible)
177
178 This signal is emitted when \l visible changes to \a visible.
179*/
180/*!
181 \qmlsignal Custom3DItem::shadowCastingChanged(bool shadowCasting)
182
183 This signal is emitted when shadowCasting changes to \a shadowCasting.
184*/
185/*!
186 \qmlsignal Custom3DItem::scalingAbsoluteChanged(bool scalingAbsolute)
187
188 This signal is emitted when scalingAbsolute changes to \a scalingAbsolute.
189*/
190
191/*!
192 * Constructs a custom 3D item with the specified \a parent.
193 */
194QCustom3DItem::QCustom3DItem(QObject *parent)
195 : QObject(*(new QCustom3DItemPrivate()), parent)
196{
197 setTextureImage(QImage());
198}
199
200/*!
201 * \internal
202 */
203QCustom3DItem::QCustom3DItem(QCustom3DItemPrivate &d, QObject *parent)
204 : QObject(d, parent)
205{
206 setTextureImage(QImage());
207}
208
209/*!
210 * Constructs a custom 3D item with the specified \a meshFile, \a position, \a
211 * scaling, \a rotation, \a texture image, and optional \a parent.
212 */
213QCustom3DItem::QCustom3DItem(const QString &meshFile,
214 QVector3D position,
215 QVector3D scaling,
216 const QQuaternion &rotation,
217 const QImage &texture,
218 QObject *parent)
219 : QObject(*(new QCustom3DItemPrivate(meshFile, position, scaling, rotation)), parent)
220{
221 setTextureImage(texture);
222}
223
224/*!
225 * Deletes the custom 3D item.
226 */
227QCustom3DItem::~QCustom3DItem() {}
228
229/*! \property QCustom3DItem::meshFile
230 *
231 * \brief The item mesh file name.
232 *
233 * The item in the file must be in mesh format. The other types
234 * can be converted by \l {Balsam Asset Import Tool}{Balsam}
235 * asset import tool. The mesh files are recommended to include
236 * vertices, normals, and UVs.
237 */
238void QCustom3DItem::setMeshFile(const QString &meshFile)
239{
240 Q_D(QCustom3DItem);
241 QFileInfo validfile(meshFile);
242
243 if (!validfile.exists() || !validfile.isFile()) {
244 qCWarning(lcProperties3D, "%s mesh file %s does not exist",
245 qUtf8Printable(QLatin1String(__FUNCTION__)), qUtf8Printable(meshFile));
246 return;
247 }
248 if (d->m_meshFile == meshFile) {
249 qCDebug(lcProperties3D, "%s value is already set to: %s",
250 qUtf8Printable(QLatin1String(__FUNCTION__)), qUtf8Printable(meshFile));
251 return;
252 }
253
254 d->m_meshFile = meshFile;
255 d->m_dirtyBits.meshDirty = true;
256 emit meshFileChanged(meshFile);
257 emit needUpdate();
258}
259
260QString QCustom3DItem::meshFile() const
261{
262 Q_D(const QCustom3DItem);
263 return d->m_meshFile;
264}
265
266/*! \property QCustom3DItem::position
267 *
268 * \brief The item position as a QVector3D.
269 *
270 * Defaults to \c {QVector3D(0.0, 0.0, 0.0)}.
271 *
272 * Item position is specified either in data coordinates or in absolute
273 * coordinates, depending on the
274 * positionAbsolute property. When using absolute coordinates, values between
275 * \c{-1.0...1.0} are within axis ranges.
276 *
277 * \note Items positioned outside any axis range are not rendered if
278 * positionAbsolute is \c{false}, unless the item is a QCustom3DVolume that
279 * would be partially visible and scalingAbsolute is also \c{false}. In that
280 * case, the visible portion of the volume will be rendered.
281 *
282 * \sa positionAbsolute
283 */
284void QCustom3DItem::setPosition(QVector3D position)
285{
286 Q_D(QCustom3DItem);
287 if (d->m_position == position) {
288 qCDebug(lcProperties3D, "%s value is already set to: %.1f %.1f %.1f",
289 qUtf8Printable(QLatin1String(__FUNCTION__)), position.x(), position.y(), position.z());
290 return;
291 }
292
293 d->m_position = position;
294 d->m_dirtyBits.positionDirty = true;
295 emit positionChanged(position);
296 emit needUpdate();
297}
298
299QVector3D QCustom3DItem::position() const
300{
301 Q_D(const QCustom3DItem);
302 return d->m_position;
303}
304
305/*! \property QCustom3DItem::positionAbsolute
306 *
307 * \brief Whether item position is to be handled in data coordinates or in
308 * absolute coordinates.
309 *
310 * Defaults to \c{false}. Items with absolute coordinates will always be
311 * rendered, whereas items with data coordinates are only rendered if they are
312 * within axis ranges.
313 *
314 * \sa position
315 */
316void QCustom3DItem::setPositionAbsolute(bool positionAbsolute)
317{
318 Q_D(QCustom3DItem);
319 if (d->m_positionAbsolute == positionAbsolute) {
320 qCDebug(lcProperties3D) << __FUNCTION__
321 << "value is already set to:" << positionAbsolute;
322 return;
323 }
324
325 d->m_positionAbsolute = positionAbsolute;
326 d->m_dirtyBits.positionDirty = true;
327 emit positionAbsoluteChanged(positionAbsolute);
328 emit needUpdate();
329}
330
331bool QCustom3DItem::isPositionAbsolute() const
332{
333 Q_D(const QCustom3DItem);
334 return d->m_positionAbsolute;
335}
336
337/*! \property QCustom3DItem::scaling
338 *
339 * \brief The item scaling as a QVector3D.
340 *
341 * Defaults to \c {QVector3D(0.1, 0.1, 0.1)}.
342 *
343 * Item scaling is either in data values or in absolute values, depending on the
344 * scalingAbsolute property. The default vector interpreted as absolute values
345 * sets the item to 10% of the height of the graph, provided the item mesh is
346 * normalized and the graph aspect ratios have not been changed from the
347 * defaults.
348 *
349 * \note In Qt 6.8 models were incorrectly assumed to be scaled to a size of 1 (-0.5...0.5)
350 * by default, when they in reality are scaled to the size of 2 (-1...1). Because of this, all
351 * custom items from Qt 6.9 onwards are twice the size compared to Qt 6.8
352 *
353 * \sa scalingAbsolute
354 */
355void QCustom3DItem::setScaling(QVector3D scaling)
356{
357 Q_D(QCustom3DItem);
358 if (d->m_scaling == scaling) {
359 qCDebug(lcProperties3D, "%s value is already set to: %.1f %.1f %.1f",
360 qUtf8Printable(QLatin1String(__FUNCTION__)), scaling.x(), scaling.y(), scaling.z());
361 return;
362 }
363
364 d->m_scaling = scaling;
365 d->m_dirtyBits.scalingDirty = true;
366 emit scalingChanged(scaling);
367 emit needUpdate();
368}
369
370QVector3D QCustom3DItem::scaling() const
371{
372 Q_D(const QCustom3DItem);
373 return d->m_scaling;
374}
375
376/*! \property QCustom3DItem::scalingAbsolute
377 *
378 * \brief Whether item scaling is to be handled in data values or in absolute
379 * values.
380 *
381 * Defaults to \c{true}.
382 *
383 * Items with absolute scaling will be rendered at the same
384 * size, regardless of axis ranges. Items with data scaling will change their
385 * apparent size according to the axis ranges. If positionAbsolute is \c{true},
386 * this property is ignored and scaling is interpreted as an absolute value. If
387 * the item has rotation, the data scaling is calculated on the unrotated item.
388 * Similarly, for QCustom3DVolume items, the range clipping is calculated on the
389 * unrotated item.
390 *
391 * \note Only absolute scaling is supported for QCustom3DLabel items or for
392 * custom items used in \l{Q3DGraphsWidgetItem::polar}{polar} graphs.
393 *
394 * \note The custom item's mesh must be normalized to the range \c{[-1 ,1]}, or
395 * the data scaling will not be accurate.
396 *
397 * \sa scaling, positionAbsolute
398 */
399void QCustom3DItem::setScalingAbsolute(bool scalingAbsolute)
400{
401 Q_D(QCustom3DItem);
402 if (d->m_isLabelItem && !scalingAbsolute) {
403 qCWarning(lcProperties3D, "%ls data bounds are not supported for label items.",
404 qUtf16Printable(QString::fromUtf8(__func__)));
405 return;
406 } else if (d->m_scalingAbsolute == scalingAbsolute) {
407 qCDebug(lcProperties3D) << __FUNCTION__
408 << "value is already set to:" << scalingAbsolute;
409 return;
410 }
411
412 d->m_scalingAbsolute = scalingAbsolute;
413 d->m_dirtyBits.scalingDirty = true;
414 emit scalingAbsoluteChanged(scalingAbsolute);
415 emit needUpdate();
416}
417
418bool QCustom3DItem::isScalingAbsolute() const
419{
420 Q_D(const QCustom3DItem);
421 return d->m_scalingAbsolute;
422}
423
424/*! \property QCustom3DItem::rotation
425 *
426 * \brief The item rotation as a QQuaternion.
427 *
428 * Defaults to \c {QQuaternion(0.0, 0.0, 0.0, 0.0)}.
429 */
430void QCustom3DItem::setRotation(const QQuaternion &rotation)
431{
432 Q_D(QCustom3DItem);
433 if (d->m_rotation == rotation) {
434 qCDebug(lcProperties3D) << __FUNCTION__
435 << "value is already set to:" << rotation;
436 return;
437 }
438
439 d->m_rotation = rotation;
440 d->m_dirtyBits.rotationDirty = true;
441 emit rotationChanged(rotation);
442 emit needUpdate();
443}
444
445QQuaternion QCustom3DItem::rotation()
446{
447 Q_D(const QCustom3DItem);
448 return d->m_rotation;
449}
450
451/*! \property QCustom3DItem::visible
452 *
453 * \brief The visibility of the item.
454 *
455 * Defaults to \c{true}.
456 */
457void QCustom3DItem::setVisible(bool visible)
458{
459 Q_D(QCustom3DItem);
460 if (d->m_visible == visible) {
461 qCDebug(lcProperties3D) << qUtf8Printable(QLatin1String(__FUNCTION__))
462 << "value is already set to:" << visible;
463 return;
464 }
465
466 d->m_visible = visible;
467 d->m_dirtyBits.visibleDirty = true;
468 emit visibleChanged(visible);
469 emit needUpdate();
470}
471
472bool QCustom3DItem::isVisible() const
473{
474 Q_D(const QCustom3DItem);
475 return d->m_visible;
476}
477
478/*! \property QCustom3DItem::shadowCasting
479 *
480 * \brief Whether shadow casting for the item is enabled.
481 *
482 * Defaults to \c{true}.
483 * If \c{false}, the item does not cast shadows regardless of
484 * Q3DGraphsWidgetItem::ShadowQuality.
485 */
486void QCustom3DItem::setShadowCasting(bool enabled)
487{
488 Q_D(QCustom3DItem);
489 if (d->m_shadowCasting == enabled) {
490 qCDebug(lcProperties3D) << __FUNCTION__
491 << "value is already set to:" << enabled;
492 return;
493 }
494
495 d->m_shadowCasting = enabled;
496 d->m_dirtyBits.shadowCastingDirty = true;
497 emit shadowCastingChanged(shadowCasting: enabled);
498 emit needUpdate();
499}
500
501bool QCustom3DItem::isShadowCasting() const
502{
503 Q_D(const QCustom3DItem);
504 return d->m_shadowCasting;
505}
506
507/*!
508 * A convenience function to construct the rotation quaternion from \a axis and
509 * \a angle.
510 *
511 * \sa rotation
512 */
513void QCustom3DItem::setRotationAxisAndAngle(QVector3D axis, float angle)
514{
515 setRotation(QQuaternion::fromAxisAndAngle(axis, angle));
516}
517
518/*!
519 * Sets the value of \a textureImage as a QImage for the item. The texture
520 * defaults to solid gray.
521 *
522 * \note To conserve memory, the given QImage is cleared after a texture is
523 * created.
524 */
525void QCustom3DItem::setTextureImage(const QImage &textureImage)
526{
527 Q_D(QCustom3DItem);
528 if (textureImage == d->m_textureImage) {
529 qCDebug(lcProperties3D) << __FUNCTION__
530 << "value is already set to:" << textureImage;
531 return;
532 }
533
534 if (textureImage.isNull()) {
535 // Make a solid gray texture
536 d->m_textureImage = QImage(2, 2, QImage::Format_RGB32);
537 d->m_textureImage.fill(color: Qt::gray);
538 } else {
539 d->m_textureImage = textureImage;
540 }
541
542 if (!d->m_textureFile.isEmpty()) {
543 d->m_textureFile.clear();
544 emit textureFileChanged(textureFile: d->m_textureFile);
545 }
546 d->m_dirtyBits.textureDirty = true;
547 emit needUpdate();
548}
549
550/*! \property QCustom3DItem::textureFile
551 *
552 * \brief The texture file name for the item.
553 *
554 * If both this property and the texture image are unset, a solid
555 * gray texture will be used.
556 *
557 * \note To conserve memory, the QImage loaded from the file is cleared after a
558 * texture is created.
559 */
560void QCustom3DItem::setTextureFile(const QString &textureFile)
561{
562 Q_D(QCustom3DItem);
563 if (d->m_textureFile == textureFile) {
564 qCDebug(lcProperties3D, "%s value is already set to: %s",
565 qUtf8Printable(QLatin1String(__FUNCTION__)), qUtf8Printable(textureFile));
566 return;
567 }
568
569 d->m_textureFile = textureFile;
570 if (!textureFile.isEmpty()) {
571 d->m_textureImage = QImage(textureFile);
572 } else {
573 d->m_textureImage = QImage(2, 2, QImage::Format_RGB32);
574 d->m_textureImage.fill(color: Qt::gray);
575 qCWarning(lcProperties3D, "%s texture file was empty, texture defaults to grey", qUtf8Printable(textureFile));
576 }
577 emit textureFileChanged(textureFile);
578 d->m_dirtyBits.textureDirty = true;
579 emit needUpdate();
580}
581
582QString QCustom3DItem::textureFile() const
583{
584 Q_D(const QCustom3DItem);
585 return d->m_textureFile;
586}
587
588QCustom3DItemPrivate::QCustom3DItemPrivate()
589 : m_textureImage(QImage(1, 1, QImage::Format_ARGB32))
590 , m_position(QVector3D(0.0f, 0.0f, 0.0f))
591 , m_positionAbsolute(false)
592 , m_scaling(QVector3D(0.1f, 0.1f, 0.1f))
593 , m_scalingAbsolute(true)
594 , m_rotation(QQuaternion())
595 , m_visible(true)
596 , m_shadowCasting(true)
597 , m_isLabelItem(false)
598 , m_isVolumeItem(false)
599{}
600
601QCustom3DItemPrivate::QCustom3DItemPrivate(const QString &meshFile,
602 QVector3D position,
603 QVector3D scaling,
604 const QQuaternion &rotation)
605 : m_textureImage(QImage(1, 1, QImage::Format_ARGB32))
606 , m_meshFile(meshFile)
607 , m_position(position)
608 , m_positionAbsolute(false)
609 , m_scaling(scaling)
610 , m_scalingAbsolute(true)
611 , m_rotation(rotation)
612 , m_visible(true)
613 , m_shadowCasting(true)
614 , m_isLabelItem(false)
615 , m_isVolumeItem(false)
616{}
617
618QCustom3DItemPrivate::~QCustom3DItemPrivate() {}
619
620QImage QCustom3DItemPrivate::textureImage()
621{
622 return m_textureImage;
623}
624
625void QCustom3DItemPrivate::clearTextureImage()
626{
627 m_textureImage = QImage();
628 m_textureFile.clear();
629}
630
631void QCustom3DItemPrivate::resetDirtyBits()
632{
633 m_dirtyBits.textureDirty = false;
634 m_dirtyBits.meshDirty = false;
635 m_dirtyBits.positionDirty = false;
636 m_dirtyBits.scalingDirty = false;
637 m_dirtyBits.rotationDirty = false;
638 m_dirtyBits.visibleDirty = false;
639 m_dirtyBits.shadowCastingDirty = false;
640}
641
642QT_END_NAMESPACE
643

source code of qtgraphs/src/graphs3d/data/qcustom3ditem.cpp