Revision control
++ // Copyright (C) 2023 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include <QtCore/qfileinfo.h>
++ #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
++
++ #include "moc_qcustom3ditem.cpp"