// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include "qcustom3ditem_p.h" #include "qgraphs3dlogging_p.h" QT_BEGIN_NAMESPACE /*! * \class QCustom3DItem * \inmodule QtGraphs * \ingroup graphs_3D * \brief The QCustom3DItem class adds a custom item to a graph. * * A custom item has a custom mesh, position, scaling, rotation, and an optional * texture. * * \sa Q3DGraphsWidgetItem::addCustomItem() */ /*! * \qmltype Custom3DItem * \inqmlmodule QtGraphs * \ingroup graphs_qml_3D * \nativetype QCustom3DItem * \brief Adds a custom item to a graph. * * A custom item has a custom mesh, position, scaling, rotation, and an optional * texture. */ /*! \qmlproperty string Custom3DItem::meshFile * * The item mesh file name. The item in the file must be mesh format. * The mesh files are recommended to include vertices, normals, and UVs. */ /*! \qmlproperty string Custom3DItem::textureFile * * The texture file name for the item. If left unset, a solid gray texture will * be used. * * \note To conserve memory, the QImage loaded from the file is cleared after a * texture is created. */ /*! \qmlproperty vector3d Custom3DItem::position * * The item position as a \l [QtQuick] vector3d. Defaults to \c {vector3d(0.0, * 0.0, 0.0)}. * * Item position is specified either in data coordinates or in absolute * coordinates, depending on the value of the positionAbsolute property. When * using absolute coordinates, values between \c{-1.0...1.0} are * within axis ranges. * * \note Items positioned outside any axis range are not rendered if * positionAbsolute is \c{false}, unless the item is a Custom3DVolume that would * be partially visible and scalingAbsolute is also \c{false}. In that case, the * visible portion of the volume will be rendered. * * \sa positionAbsolute, scalingAbsolute */ /*! \qmlproperty bool Custom3DItem::positionAbsolute * * Defines whether item position is to be handled in data coordinates or in * absolute coordinates. Defaults to \c{false}. Items with absolute coordinates * will always be rendered, whereas items with data coordinates are only * rendered if they are within axis ranges. * * \sa position */ /*! \qmlproperty vector3d Custom3DItem::scaling * * The item scaling as a \l [QtQuick] vector3d type. Defaults to * \c {vector3d(0.1, 0.1, 0.1)}. * * Item scaling is specified either in data values or in absolute values, * depending on the value of the scalingAbsolute property. The default vector * interpreted as absolute values sets the item to * 10% of the height of the graph, provided the item mesh is normalized and the * graph aspect ratios have not been changed from the defaults. * * \note Only absolute scaling is supported for Custom3DLabel items or for * custom items used in \l{GraphsItem3D::polar}{polar} graphs. * * \note In Qt 6.8 models were incorrectly assumed to be scaled to a size of 1 (-0.5...0.5) * by default, when they in reality are scaled to the size of 2 (-1...1). Because of this, all * custom items from Qt 6.9 onwards are twice the size compared to Qt 6.8 * * \sa scalingAbsolute */ /*! \qmlproperty bool Custom3DItem::scalingAbsolute * * Defines whether item scaling is to be handled in data values or in absolute * values. Defaults to \c{true}. Items with absolute scaling will be rendered at * the same size, regardless of axis ranges. Items with data scaling will change * their apparent size according to the axis ranges. If positionAbsolute is * \c{true}, this property is ignored and scaling is interpreted as an absolute * value. If the item has rotation, the data scaling is calculated on the * unrotated item. Similarly, for Custom3DVolume items, the range clipping is * calculated on the unrotated item. * * \note Only absolute scaling is supported for Custom3DLabel items or for * custom items used in \l{GraphsItem3D::polar}{polar} graphs. * * \note The custom item's mesh must be normalized to the range \c{[-1 ,1]}, or * the data scaling will not be accurate. * * \sa scaling, positionAbsolute */ /*! \qmlproperty quaternion Custom3DItem::rotation * * The item rotation as a \l [QtQuick] quaternion. Defaults to * \c {quaternion(0.0, 0.0, 0.0, 0.0)}. */ /*! \qmlproperty bool Custom3DItem::rotationAbsolute * \since 6.11 * * Defines whether item rotation is to be handled in data values or in absolute * values. Defaults to \c{true}. Items with absolute rotation will be rotated with * the default coordinates, regardless of axis. Items with data rotation will rotate * according to the axis coordinates. * * \sa rotation */ /*! \qmlproperty bool Custom3DItem::visible * * The visibility of the item. Defaults to \c{true}. */ /*! \qmlproperty bool Custom3DItem::shadowCasting * * Defines whether shadow casting for the item is enabled. Defaults to \c{true}. * If \c{false}, the item does not cast shadows regardless of * \l{QtGraphs3D::ShadowQuality}{ShadowQuality}. */ /*! * \qmlmethod void Custom3DItem::setRotationAxisAndAngle(vector3d axis, real * angle) * * A convenience function to construct the rotation quaternion from \a axis and * \a angle. * * \sa rotation */ /*! \qmlsignal Custom3DItem::meshFileChanged(string meshFile) This signal is emitted when meshFile changes to \a meshFile. */ /*! \qmlsignal Custom3DItem::textureFileChanged(string textureFile) This signal is emitted when textureFile changes to \a textureFile. */ /*! \qmlsignal Custom3DItem::positionChanged(vector3d position) This signal is emitted when item \l position changes to \a position. */ /*! \qmlsignal Custom3DItem::positionAbsoluteChanged(bool positionAbsolute) This signal is emitted when positionAbsolute changes to \a positionAbsolute. */ /*! \qmlsignal Custom3DItem::scalingChanged(vector3d scaling) This signal is emitted when \l scaling changes to \a scaling. */ /*! \qmlsignal Custom3DItem::rotationChanged(quaternion rotation) This signal is emitted when \l rotation changes to \a rotation. */ /*! \qmlsignal Custom3DItem::visibleChanged(bool visible) This signal is emitted when \l visible changes to \a visible. */ /*! \qmlsignal Custom3DItem::shadowCastingChanged(bool shadowCasting) This signal is emitted when shadowCasting changes to \a shadowCasting. */ /*! \qmlsignal Custom3DItem::scalingAbsoluteChanged(bool scalingAbsolute) This signal is emitted when scalingAbsolute changes to \a scalingAbsolute. */ /*! * Constructs a custom 3D item with the specified \a parent. */ QCustom3DItem::QCustom3DItem(QObject *parent) : QObject(*(new QCustom3DItemPrivate()), parent) { setTextureImage(QImage()); } /*! * \internal */ QCustom3DItem::QCustom3DItem(QCustom3DItemPrivate &d, QObject *parent) : QObject(d, parent) { setTextureImage(QImage()); } /*! * Constructs a custom 3D item with the specified \a meshFile, \a position, \a * scaling, \a rotation, \a texture image, and optional \a parent. */ QCustom3DItem::QCustom3DItem(const QString &meshFile, QVector3D position, QVector3D scaling, const QQuaternion &rotation, const QImage &texture, QObject *parent) : QObject(*(new QCustom3DItemPrivate(meshFile, position, scaling, rotation)), parent) { setTextureImage(texture); } /*! * Deletes the custom 3D item. */ QCustom3DItem::~QCustom3DItem() {} /*! \property QCustom3DItem::meshFile * * \brief The item mesh file name. * * The item in the file must be in mesh format. The other types * can be converted by \l {Balsam Asset Import Tool}{Balsam} * asset import tool. The mesh files are recommended to include * vertices, normals, and UVs. */ void QCustom3DItem::setMeshFile(const QString &meshFile) { Q_D(QCustom3DItem); QFileInfo validfile(meshFile); if (!validfile.exists() || !validfile.isFile()) { qCWarning(lcProperties3D, "%s mesh file %s does not exist", qUtf8Printable(QLatin1String(__func__)), qUtf8Printable(meshFile)); return; } if (d->m_meshFile == meshFile) { qCDebug(lcProperties3D, "%s value is already set to: %s", qUtf8Printable(QLatin1String(__func__)), qUtf8Printable(meshFile)); return; } d->m_meshFile = meshFile; d->m_dirtyBits.meshDirty = true; emit meshFileChanged(meshFile); emit needUpdate(); } QString QCustom3DItem::meshFile() const { Q_D(const QCustom3DItem); return d->m_meshFile; } /*! \property QCustom3DItem::position * * \brief The item position as a QVector3D. * * Defaults to \c {QVector3D(0.0, 0.0, 0.0)}. * * Item position is specified either in data coordinates or in absolute * coordinates, depending on the * positionAbsolute property. When using absolute coordinates, values between * \c{-1.0...1.0} are within axis ranges. * * \note Items positioned outside any axis range are not rendered if * positionAbsolute is \c{false}, unless the item is a QCustom3DVolume that * would be partially visible and scalingAbsolute is also \c{false}. In that * case, the visible portion of the volume will be rendered. * * \sa positionAbsolute */ void QCustom3DItem::setPosition(QVector3D position) { Q_D(QCustom3DItem); if (d->m_position == position) { qCDebug(lcProperties3D, "%s value is already set to: %.1f %.1f %.1f", qUtf8Printable(QLatin1String(__FUNCTION__)), position.x(), position.y(), position.z()); return; } d->m_position = position; d->m_dirtyBits.positionDirty = true; emit positionChanged(position); emit needUpdate(); } QVector3D QCustom3DItem::position() const { Q_D(const QCustom3DItem); return d->m_position; } /*! \property QCustom3DItem::positionAbsolute * * \brief Whether item position is to be handled in data coordinates or in * absolute coordinates. * * Defaults to \c{false}. Items with absolute coordinates will always be * rendered, whereas items with data coordinates are only rendered if they are * within axis ranges. * * \sa position */ void QCustom3DItem::setPositionAbsolute(bool positionAbsolute) { Q_D(QCustom3DItem); if (d->m_positionAbsolute == positionAbsolute) { qCDebug(lcProperties3D) << __FUNCTION__ << "value is already set to:" << positionAbsolute; return; } d->m_positionAbsolute = positionAbsolute; d->m_dirtyBits.positionDirty = true; emit positionAbsoluteChanged(positionAbsolute); emit needUpdate(); } bool QCustom3DItem::isPositionAbsolute() const { Q_D(const QCustom3DItem); return d->m_positionAbsolute; } /*! \property QCustom3DItem::scaling * * \brief The item scaling as a QVector3D. * * Defaults to \c {QVector3D(0.1, 0.1, 0.1)}. * * Item scaling is either in data values or in absolute values, depending on the * scalingAbsolute property. The default vector interpreted as absolute values * sets the item to 10% of the height of the graph, provided the item mesh is * normalized and the graph aspect ratios have not been changed from the * defaults. * * \note In Qt 6.8 models were incorrectly assumed to be scaled to a size of 1 (-0.5...0.5) * by default, when they in reality are scaled to the size of 2 (-1...1). Because of this, all * custom items from Qt 6.9 onwards are twice the size compared to Qt 6.8 * * \sa scalingAbsolute */ void QCustom3DItem::setScaling(QVector3D scaling) { Q_D(QCustom3DItem); if (d->m_scaling == scaling) { qCDebug(lcProperties3D, "%s value is already set to: %.1f %.1f %.1f", qUtf8Printable(QLatin1String(__FUNCTION__)), scaling.x(), scaling.y(), scaling.z()); return; } d->m_scaling = scaling; d->m_dirtyBits.scalingDirty = true; emit scalingChanged(scaling); emit needUpdate(); } QVector3D QCustom3DItem::scaling() const { Q_D(const QCustom3DItem); return d->m_scaling; } /*! \property QCustom3DItem::scalingAbsolute * * \brief Whether item scaling is to be handled in data values or in absolute * values. * * Defaults to \c{true}. * * Items with absolute scaling will be rendered at the same * size, regardless of axis ranges. Items with data scaling will change their * apparent size according to the axis ranges. If positionAbsolute is \c{true}, * this property is ignored and scaling is interpreted as an absolute value. If * the item has rotation, the data scaling is calculated on the unrotated item. * Similarly, for QCustom3DVolume items, the range clipping is calculated on the * unrotated item. * * \note Only absolute scaling is supported for QCustom3DLabel items or for * custom items used in \l{Q3DGraphsWidgetItem::polar}{polar} graphs. * * \note The custom item's mesh must be normalized to the range \c{[-1 ,1]}, or * the data scaling will not be accurate. * * \sa scaling, positionAbsolute */ void QCustom3DItem::setScalingAbsolute(bool scalingAbsolute) { Q_D(QCustom3DItem); if (d->m_isLabelItem && !scalingAbsolute) { qCWarning(lcProperties3D, "%ls data bounds are not supported for label items.", qUtf16Printable(QString::fromUtf8(__func__))); return; } else if (d->m_scalingAbsolute == scalingAbsolute) { qCDebug(lcProperties3D) << __FUNCTION__ << "value is already set to:" << scalingAbsolute; return; } d->m_scalingAbsolute = scalingAbsolute; d->m_dirtyBits.scalingDirty = true; emit scalingAbsoluteChanged(scalingAbsolute); emit needUpdate(); } bool QCustom3DItem::isScalingAbsolute() const { Q_D(const QCustom3DItem); return d->m_scalingAbsolute; } /*! \property QCustom3DItem::rotation * * \brief The item rotation as a QQuaternion. * * Defaults to \c {QQuaternion(0.0, 0.0, 0.0, 0.0)}. */ void QCustom3DItem::setRotation(const QQuaternion &rotation) { Q_D(QCustom3DItem); if (d->m_rotation == rotation) { qCDebug(lcProperties3D) << __FUNCTION__ << "value is already set to:" << rotation; return; } d->m_rotation = rotation; d->m_dirtyBits.rotationDirty = true; emit rotationChanged(rotation); emit needUpdate(); } QQuaternion QCustom3DItem::rotation() { Q_D(const QCustom3DItem); return d->m_rotation; } /*! \property QCustom3DItem::rotationAbsolute * \since 6.11 * * \brief Whether item rotation is to be handled in data axis coordinates or in absolute * coordinates. * * Defines whether item rotation is to be handled in data values or in absolute * values. Defaults to \c{true}. Items with absolute rotation will be rotated with * the default coordinates, regardless of axis. Items with data rotation will rotate * according to the axis coordinates. * * \sa rotation */ void QCustom3DItem::setRotationAbsolute(bool rotationAbsolute) { Q_D(QCustom3DItem); if (d->m_rotationAbsolute == rotationAbsolute) { qCDebug(lcProperties3D) << __FUNCTION__ << "value is already set to:" << rotationAbsolute; return; } d->m_rotationAbsolute = rotationAbsolute; d->m_dirtyBits.rotationDirty = true; emit rotationAbsoluteChanged(rotationAbsolute); emit needUpdate(); } bool QCustom3DItem::isRotationAbsolute() const { Q_D(const QCustom3DItem); return d->m_rotationAbsolute; } /*! \property QCustom3DItem::visible * * \brief The visibility of the item. * * Defaults to \c{true}. */ void QCustom3DItem::setVisible(bool visible) { Q_D(QCustom3DItem); if (d->m_visible == visible) { qCDebug(lcProperties3D) << qUtf8Printable(QLatin1String(__FUNCTION__)) << "value is already set to:" << visible; return; } d->m_visible = visible; d->m_dirtyBits.visibleDirty = true; emit visibleChanged(visible); emit needUpdate(); } bool QCustom3DItem::isVisible() const { Q_D(const QCustom3DItem); return d->m_visible; } /*! \property QCustom3DItem::shadowCasting * * \brief Whether shadow casting for the item is enabled. * * Defaults to \c{true}. * If \c{false}, the item does not cast shadows regardless of * Q3DGraphsWidgetItem::ShadowQuality. */ void QCustom3DItem::setShadowCasting(bool enabled) { Q_D(QCustom3DItem); if (d->m_shadowCasting == enabled) { qCDebug(lcProperties3D) << __FUNCTION__ << "value is already set to:" << enabled; return; } d->m_shadowCasting = enabled; d->m_dirtyBits.shadowCastingDirty = true; emit shadowCastingChanged(enabled); emit needUpdate(); } bool QCustom3DItem::isShadowCasting() const { Q_D(const QCustom3DItem); return d->m_shadowCasting; } /*! * A convenience function to construct the rotation quaternion from \a axis and * \a angle. * * \sa rotation */ void QCustom3DItem::setRotationAxisAndAngle(QVector3D axis, float angle) { setRotation(QQuaternion::fromAxisAndAngle(axis, angle)); } /*! * Sets the value of \a textureImage as a QImage for the item. The texture * defaults to solid gray. * * \note To conserve memory, the given QImage is cleared after a texture is * created. */ void QCustom3DItem::setTextureImage(const QImage &textureImage) { Q_D(QCustom3DItem); if (textureImage == d->m_textureImage) { qCDebug(lcProperties3D) << __FUNCTION__ << "value is already set to:" << textureImage; return; } if (textureImage.isNull()) { // Make a solid gray texture d->m_textureImage = QImage(2, 2, QImage::Format_RGB32); d->m_textureImage.fill(Qt::gray); } else { d->m_textureImage = textureImage; } if (!d->m_textureFile.isEmpty()) { d->m_textureFile.clear(); emit textureFileChanged(d->m_textureFile); } d->m_dirtyBits.textureDirty = true; emit needUpdate(); } /*! \property QCustom3DItem::textureFile * * \brief The texture file name for the item. * * If both this property and the texture image are unset, a solid * gray texture will be used. * * \note To conserve memory, the QImage loaded from the file is cleared after a * texture is created. */ void QCustom3DItem::setTextureFile(const QString &textureFile) { Q_D(QCustom3DItem); if (d->m_textureFile == textureFile) { qCDebug(lcProperties3D, "%s value is already set to: %s", qUtf8Printable(QLatin1String(__FUNCTION__)), qUtf8Printable(textureFile)); return; } d->m_textureFile = textureFile; if (!textureFile.isEmpty()) { d->m_textureImage = QImage(textureFile); } else { d->m_textureImage = QImage(2, 2, QImage::Format_RGB32); d->m_textureImage.fill(Qt::gray); qCWarning(lcProperties3D, "%s texture file was empty, texture defaults to grey", qUtf8Printable(textureFile)); } emit textureFileChanged(textureFile); d->m_dirtyBits.textureDirty = true; emit needUpdate(); } QString QCustom3DItem::textureFile() const { Q_D(const QCustom3DItem); return d->m_textureFile; } QCustom3DItemPrivate::QCustom3DItemPrivate() : m_textureImage(QImage(1, 1, QImage::Format_ARGB32)) , m_position(QVector3D(0.0f, 0.0f, 0.0f)) , m_positionAbsolute(false) , m_scaling(QVector3D(0.1f, 0.1f, 0.1f)) , m_scalingAbsolute(true) , m_rotation(QQuaternion()) , m_visible(true) , m_shadowCasting(true) , m_isLabelItem(false) , m_isVolumeItem(false) {} QCustom3DItemPrivate::QCustom3DItemPrivate(const QString &meshFile, QVector3D position, QVector3D scaling, const QQuaternion &rotation) : m_textureImage(QImage(1, 1, QImage::Format_ARGB32)) , m_meshFile(meshFile) , m_position(position) , m_positionAbsolute(false) , m_scaling(scaling) , m_scalingAbsolute(true) , m_rotation(rotation) , m_rotationAbsolute(true) , m_visible(true) , m_shadowCasting(true) , m_isLabelItem(false) , m_isVolumeItem(false) {} QCustom3DItemPrivate::~QCustom3DItemPrivate() {} QImage QCustom3DItemPrivate::textureImage() { return m_textureImage; } void QCustom3DItemPrivate::clearTextureImage() { m_textureImage = QImage(); m_textureFile.clear(); } void QCustom3DItemPrivate::resetDirtyBits() { m_dirtyBits.textureDirty = false; m_dirtyBits.meshDirty = false; m_dirtyBits.positionDirty = false; m_dirtyBits.scalingDirty = false; m_dirtyBits.rotationDirty = false; m_dirtyBits.visibleDirty = false; m_dirtyBits.shadowCastingDirty = false; } QT_END_NAMESPACE