Add spline rendering to scatter

Also adding manual and autotests

Fixes: QTBUG-125916
Fixes: QTBUG-124742
Change-Id: I152e1d3652c9c97cb3e714e0a80acdfc9e4f9482
Reviewed-by: Tomi Korpipää <tomi.korpipaa@qt.io>
Reviewed-by: Sami Varanka <sami.varanka@qt.io>
This commit is contained in:
Sakaria Pouke 2024-07-24 14:06:07 +03:00
parent 1728887e0a
commit ee5ede3108
23 changed files with 1764 additions and 5 deletions

View File

@ -56,6 +56,7 @@ else()
"graphs3d/qml/resources/SurfaceMaterial.qml"
"graphs3d/qml/resources/ScatterMaterial.qml"
"graphs3d/qml/resources/ScatterMaterialInstancing.qml"
"graphs3d/qml/resources/SplineMaterial.qml"
"graphs3d/qml/resources/GridSurfaceMaterial.qml"
"graphs3d/qml/resources/SurfaceSliceMaterial.qml"
"graphs3d/qml/resources/SurfaceShadowNoTex.qml"
@ -144,6 +145,8 @@ else()
"graphs3d/engine/shaders/scatterinstancing.vert"
"graphs3d/engine/shaders/scatterinstancing.frag"
"graphs3d/engine/shaders/surfaceSlice.vert"
"graphs3d/engine/shaders/spline.vert"
"graphs3d/engine/shaders/spline.frag"
"graphs3d/engine/shaders/texture3d.frag"
"graphs3d/engine/shaders/texture3d.vert"
"graphs3d/engine/shaders/texture3dlowdef.frag"

View File

@ -69,6 +69,7 @@ else()
SOURCES
data/qitemmodelscatterdataproxy.cpp data/qitemmodelscatterdataproxy.h data/qitemmodelscatterdataproxy_p.h
data/qscatter3dseries.cpp data/qscatter3dseries.h data/qscatter3dseries_p.h
data/qspline3dseries.cpp data/qspline3dseries.h data/qspline3dseries_p.h
data/qscatterdataitem.cpp data/qscatterdataitem.h
data/qscatterdataproxy.cpp data/qscatterdataproxy.h data/qscatterdataproxy_p.h
data/scatteritemmodelhandler.cpp data/scatteritemmodelhandler_p.h
@ -78,6 +79,7 @@ else()
qml/foreigntypesscatter_p.h
qml/qquickgraphsscatter.cpp qml/qquickgraphsscatter_p.h
qml/qquickgraphsscatterseries.cpp qml/qquickgraphsscatterseries_p.h
qml/qquickgraphssplineseries.cpp qml/qquickgraphssplineseries_p.h
INCLUDE_DIRECTORIES
data
engine

View File

@ -0,0 +1,340 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <private/qspline3dseries_p.h>
QT_BEGIN_NAMESPACE
/*!
* \class QSpline3DSeries
* \inmodule QtGraphs
* \ingroup graphs_3D
* \since 6.9
* \brief The QSpline3DSeries class represents a data series as a spline.
*
* Spline graphs are used to show information as a series of data points connected
* by a curved or straight Catmull-Rom spline.
*
* This class manages the spline specific visual elements.
*
* Spline3DSeries extends the Scatter3DSeries API.
*/
/*!
* \qmltype Spline3DSeries
* \instantiates QSpline3DSeries
* \inqmlmodule QtGraphs
* \ingroup graphs_qml_3D
* \inherits Scatter3DSeries
* \since 6.9
* \brief Represents a data series in a 3D spline graph.
*
* Spline graphs are used to show information as a series of data points connected
* by a curved or straight Catmull-Rom spline.
*
* This type manages the spline specific visual elements.
*
*/
/*!
* \qmlproperty bool Spline3DSeries::splineVisible
*
* Visibility of the spline. The default value is \c true.
*/
/*!
* \qmlproperty float Spline3DSeries::splineTension
*
* The tension of the spline.
*
* The spline uses maximum curvature for segments at a value of \c 0.0
* Segments are completely straight at a value of \c 1.0
* Must be between \c 0.0 and \c 1.0
* The default value is \c 0.0
*
*/
/*!
* \qmlproperty float Spline3DSeries::splineKnotting
*
* The knot parametrization of the spline.
*
* This parameter can change the profile of the curve.
* The spline is classified as a uniform Catmull-Rom spline at a value of \c 0.0,
* a centripetal Catmull-Rom spline at a value of \c 0.5,
* and a chordal Catmull-Rom spline at a value of \c 1.0.
*
* The value must be between \c 0.0 and \c 1.0.
* The default value is \c 0.5.
*
*/
/*!
* \qmlproperty float Spline3DSeries::splineLooping
*
* Determines whether the spline loops.
*
* This adds a spline segment between the first and last points of the series
* connecting the spline into a loop.
*
* The default value is \c false
*
*/
/*!
* \qmlproperty int QSpline3DSeries::splineResolution
*
* The resolution of the segments spline.
*
* The number of vertices per spline segment,
* which is defined as the part between two points.
*
* Must be a value above \c 2.
* The default value is \c 10.
*/
/*!
* \qmlproperty color Spline3DSeries::splineColor
*
* The color of the spline.
*
*/
/*!
\qmlsignal Scatter3DSeries::splineVisibilityChanged(bool visible)
This signal is emitted when splineVisible changes to \a visible.
*/
/*!
\qmlsignal Scatter3DSeries::splineTensionChanged(float tension)
This signal is emitted when splineTension changes to \a tension.
*/
/*!
\qmlsignal Scatter3DSeries::splineKnottingChanged(float knotting)
This signal is emitted when splineKnotting changes to \a knotting.
*/
/*!
\qmlsignal Scatter3DSeries::splineLoopingChanged(bool looping)
This signal is emitted when splineLooping changes to \a looping.
*/
/*!
\qmlsignal Scatter3DSeries::splineColorChanged(color color)
This signal is emitted when splineColor changes to \a color.
*/
/*!
\qmlsignal Scatter3DSeries::splineResolutionChanged(int resolution)
This signal is emitted when splineResolution changes to \a resolution.
*/
/*!
* Constructs a spline 3D series with the parent \a parent.
*/
QSpline3DSeries ::QSpline3DSeries(QObject *parent)
: QScatter3DSeries(*(new QSpline3DSeriesPrivate()), parent)
{
Q_D(QScatter3DSeries);
// Default proxy
d->setDataProxy(new QScatterDataProxy);
}
/*!
* Constructs a spline 3D series with the data proxy \a dataProxy and the
* parent \a parent.
*/
QSpline3DSeries ::QSpline3DSeries(QScatterDataProxy *dataProxy, QObject *parent)
: QScatter3DSeries(*(new QSpline3DSeriesPrivate()), parent)
{
Q_D(QScatter3DSeries);
// Default proxy
d->setDataProxy(dataProxy);
}
/*!
* \internal
*/
QSpline3DSeries ::QSpline3DSeries(QSpline3DSeriesPrivate &dd, QObject *parent)
: QScatter3DSeries(dd, parent)
{}
/*!
* Deletes the spline 3D series.
*/
QSpline3DSeries::~QSpline3DSeries() {}
/*!
* \property QSpline3DSeries::splineVisible
*
* \brief Visibility of the spline.
*
* Visibility of the spline.
* The default value is \c true.
*
*/
void QSpline3DSeries::setSplineVisible(bool visible)
{
Q_D(QSpline3DSeries);
if (d->m_splineVisible != visible) {
d->m_splineVisible = visible;
emit splineVisibilityChanged(visible);
}
}
bool QSpline3DSeries::isSplineVisible() const
{
const Q_D(QSpline3DSeries);
return d->m_splineVisible;
}
/*!
* \property QSpline3DSeries::splineTension
*
* \brief The tension of the spline.
*
* The spline uses maximum curvature for segments at a value of \c 0.0
* Segments are completely straight at a value of \c 1.0
* Must be between \c 0.0 and \c 1.0
* The default value is \c 0.0
*
*/
void QSpline3DSeries::setSplineTension(float tension)
{
Q_D(QSpline3DSeries);
if (tension < 0.0f || tension > 1.0f) {
qWarning("Invalid tension. Valid range for tension is 0.0f...1.0f");
} else if (d->m_tension != tension) {
d->m_tension = tension;
emit splineTensionChanged(tension);
}
}
float QSpline3DSeries::splineTension() const
{
const Q_D(QSpline3DSeries);
return d->m_tension;
}
/*!
* \property QSpline3DSeries::splineKnotting
*
* \brief The knot parametrization of the spline.
*
* This parameter can change the profile of the curve.
* The spline is classified as a uniform Catmull-Rom spline at a value of \c 0.0,
* a centripetal Catmull-Rom spline at a value of \c 0.5,
* and a chordal Catmull-Rom spline at a value of \c 1.0.
*
* The value must be between \c 0.0 and \c 1.0.
* The default value is \c 0.5.
*
*/
void QSpline3DSeries::setSplineKnotting(float knotting)
{
Q_D(QSpline3DSeries);
if (knotting < 0.0f || knotting > 1.0f) {
qWarning("Invalid knotting. Valid range for knotting is 0.0f...1.0f");
} else if (d->m_knotting != knotting) {
d->m_knotting = knotting;
emit splineKnottingChanged(knotting);
}
}
float QSpline3DSeries::splineKnotting() const
{
const Q_D(QSpline3DSeries);
return d->m_knotting;
}
/*!
* \property QSpline3DSeries::splineLooping
*
* \brief Determines whether the spline loops.
*
* This adds a spline segment between the first and last points of the series
* connecting the spline into a loop.
*
* The default value is \c false
*
*/
void QSpline3DSeries::setSplineLooping(bool looping)
{
Q_D(QSpline3DSeries);
if (d->m_looping != looping) {
d->m_looping = looping;
emit splineLoopingChanged(looping);
}
}
bool QSpline3DSeries::isSplineLooping() const
{
const Q_D(QSpline3DSeries);
return d->m_looping;
}
/*!
* \property QSpline3DSeries::splineColor
*
* \brief The color of the spline.
*
*/
void QSpline3DSeries::setSplineColor(QColor color)
{
Q_D(QSpline3DSeries);
if (d->m_splineColor != color) {
d->m_splineColor = color;
emit splineColorChanged(color);
}
}
QColor QSpline3DSeries::splineColor() const
{
const Q_D(QSpline3DSeries);
return d->m_splineColor;
}
/*!
* \property QSpline3DSeries::splineResolution
*
* \brief The resolution of the segments spline.
*
* The number of vertices per spline segment,
* which is defined as the part between two points.
*
* Must be a value above \c 2.
* The default value is \c 10.
*/
void QSpline3DSeries::setSplineResolution(int resolution)
{
Q_D(QSpline3DSeries);
if (resolution < 2) {
qWarning("Invalid resolution. The resolution must be 2 or above");
} else if (d->m_resolution != resolution) {
d->m_resolution = resolution;
emit splineResolutionChanged(resolution);
}
}
int QSpline3DSeries::splineResolution() const
{
const Q_D(QSpline3DSeries);
return d->m_resolution;
}
// QSpline3DSeriesPrivate
QSpline3DSeriesPrivate::QSpline3DSeriesPrivate()
: QScatter3DSeriesPrivate()
{}
QSpline3DSeriesPrivate::~QSpline3DSeriesPrivate() {}
QT_END_NAMESPACE

View File

@ -0,0 +1,69 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef QSPLINE3DSERIES_H
#define QSPLINE3DSERIES_H
#include <QtGraphs/qscatter3dseries.h>
QT_BEGIN_NAMESPACE
class QSpline3DSeriesPrivate;
class Q_GRAPHS_EXPORT QSpline3DSeries : public QScatter3DSeries
{
Q_OBJECT
Q_DECLARE_PRIVATE(QSpline3DSeries)
Q_PROPERTY(bool splineVisible READ isSplineVisible WRITE setSplineVisible NOTIFY
splineVisibilityChanged FINAL)
Q_PROPERTY(float splineTension READ splineTension WRITE setSplineTension NOTIFY
splineTensionChanged FINAL)
Q_PROPERTY(float splineKnotting READ splineKnotting WRITE setSplineKnotting NOTIFY
splineKnottingChanged FINAL)
Q_PROPERTY(bool splineLooping READ isSplineLooping WRITE setSplineLooping NOTIFY
splineLoopingChanged FINAL)
Q_PROPERTY(
QColor splineColor READ splineColor WRITE setSplineColor NOTIFY splineColorChanged FINAL)
Q_PROPERTY(int splineResolution READ splineResolution WRITE setSplineResolution NOTIFY
splineResolutionChanged FINAL)
public:
explicit QSpline3DSeries(QObject *parent = nullptr);
explicit QSpline3DSeries(QScatterDataProxy *dataProxy, QObject *parent = nullptr);
~QSpline3DSeries() override;
void setSplineVisible(bool draw);
bool isSplineVisible() const;
void setSplineTension(float tension);
float splineTension() const;
void setSplineKnotting(float knotting);
float splineKnotting() const;
void setSplineLooping(bool looping);
bool isSplineLooping() const;
void setSplineColor(QColor color);
QColor splineColor() const;
void setSplineResolution(int resolution);
int splineResolution() const;
Q_SIGNALS:
void splineVisibilityChanged(bool visible);
void splineTensionChanged(float tension);
void splineKnottingChanged(float knotting);
void splineLoopingChanged(bool looping);
void splineColorChanged(QColor color);
void splineResolutionChanged(int resolution);
protected:
explicit QSpline3DSeries(QSpline3DSeriesPrivate &d, QObject *parent = nullptr);
private:
Q_DISABLE_COPY_MOVE(QSpline3DSeries)
friend class QQuickGraphsScatter;
};
QT_END_NAMESPACE
#endif // QSPLINE3DSERIES_H

View File

@ -0,0 +1,40 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// W A R N I N G
// -------------
//
// This file is not part of the Qt Graphs API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
#ifndef QSPLINE3DSERIES_P_H
#define QSPLINE3DSERIES_P_H
#include <QtGraphs/qspline3dseries.h>
#include <private/qscatter3dseries_p.h>
QT_BEGIN_NAMESPACE
class QSpline3DSeriesPrivate : public QScatter3DSeriesPrivate
{
Q_DECLARE_PUBLIC(QSpline3DSeries)
public:
QSpline3DSeriesPrivate();
~QSpline3DSeriesPrivate() override;
private:
bool m_splineVisible = true;
qreal m_tension = 0;
qreal m_knotting = 0.5;
bool m_looping = false;
QColor m_splineColor = QColor(255, 0, 0);
qsizetype m_resolution = 10;
};
QT_END_NAMESPACE
#endif // QSPLINE3DSERIES_P_H

View File

@ -64,11 +64,11 @@
\section2 3D Scatter Graphs
3D scatter graphs present data as a collection of points. The
3D scatter graphs present data as a collection of points or a spline. The
\l Q3DScatterWidgetItem class is used to create a graph. The
\l QScatter3DSeries and \l QScatterDataProxy classes are used to set data to
\l QScatter3DSeries or \l QSpline3DSeries and \l QScatterDataProxy classes are used to set data to
the graph and to control the visual properties of the graph. In QML, the
corresponding types are \l Scatter3D, \l Scatter3DSeries, and
corresponding types are \l Scatter3D, \l Scatter3DSeries, \l Spline3DSeries, and
\l ScatterDataProxy.
\image q3dscatter-minimal.png

View File

@ -0,0 +1,6 @@
void MAIN(){
}
void POST_PROCESS() {
COLOR_SUM = color;
}

View File

@ -0,0 +1,50 @@
void MAIN() {
//Catmull-Rom spline
float curveIndex = UV0.y;
float step = 1.0f / float(points);
//add small offset to avoid sampling the edge of each texture pixel
float pixelOffset = step * 0.1f;
vec3 p0 = texture(controlPoints, vec2(curveIndex + pixelOffset, 0.5f)).xyz;
vec3 p1 = texture(controlPoints, vec2(curveIndex + step + pixelOffset , 0.5f)).xyz;
vec3 p2 = texture(controlPoints, vec2(curveIndex + 2.0f*step + pixelOffset , 0.5f)).xyz;
vec3 p3 = texture(controlPoints, vec2(curveIndex + 3.0f*step + pixelOffset , 0.5f)).xyz;
// check if looping segment
float lastThresh = 1.0f - (1.0f / float(points - 2.0f)) - 0.001f;
bool lastSegment = (curveIndex * points) / (points - 2.0f) >= lastThresh;
if (loop && lastSegment) {
p2 = texture(controlPoints, vec2(step, 0.5f)).xyz;
p3 = texture(controlPoints, vec2(2.0f* step, 0.5f)).xyz;
}
float t01 = pow(distance(p0, p1), knotting);
float t12 = pow(distance(p1, p2), knotting);
float t23 = pow(distance(p2, p3), knotting);
vec3 m1 = (1.0f - tension) *
(p2 - p1 + t12 * ((p1 - p0) / t01 - (p2 - p0) / (t01 + t12)));
vec3 m2 = (1.0f - tension) *
(p2 - p1 + t12 * ((p3 - p2) / t23 - (p3 - p1) / (t12 + t23)));
vec3 A = 2.0f * (p1 - p2) + m1 + m2;
vec3 B = -3.0f * (p1 - p2) - m1 - m1 - m2;
vec3 C = m1;
vec3 D = p1;
float t = UV0.x;
vec3 point =
A * t * t * t +
B * t * t +
C * t +
D;
vec4 pos = MODELVIEWPROJECTION_MATRIX * vec4(point, 1.0f);
POSITION = pos;
}

View File

@ -42,7 +42,7 @@ static const int insertRemoveRecordReserveSize = 31;
*
* See \l{Simple Scatter Graph} for more thorough usage example.
*
* \sa Scatter3DSeries, ScatterDataProxy, Bars3D, Surface3D,
* \sa Scatter3DSeries, Spline3DSeries, ScatterDataProxy, Bars3D, Surface3D,
* {Qt Graphs C++ Classes for 3D}
*/
@ -704,11 +704,13 @@ void QQuickGraphsScatter::removeDataItems(ScatterModel *graphModel,
deleteDataItem(graphModel->selectionIndicator);
deleteDataItem(graphModel->baseRef);
deleteDataItem(graphModel->selectionRef);
deleteDataItem(graphModel->splineModel);
graphModel->instancingRootItem = nullptr;
graphModel->selectionIndicator = nullptr;
graphModel->baseRef = nullptr;
graphModel->selectionRef = nullptr;
graphModel->splineModel = nullptr;
} else {
QList<QQuick3DModel *> &items = graphModel->dataItems;
removeDataItems(items, items.count());
@ -1529,6 +1531,177 @@ void QQuickGraphsScatter::calculatePolarXZ(const float posX,
z = -static_cast<float>(radius * qCos(angle)) * m_polarRadius;
}
void QQuickGraphsScatter::updateSpline(ScatterModel *model)
{
if (auto series = qobject_cast<QSpline3DSeries *>(model->series)) {
if (!series->isSplineVisible()) {
if (model->splineModel)
model->splineModel->setVisible(false);
return;
} else {
if (!model->splineModel)
createSplineModel(model);
QQmlListReference materialRef(model->splineModel, "materials");
QQuick3DCustomMaterial *material = qobject_cast<QQuick3DCustomMaterial *>(
materialRef.at(0));
QVariant splineInputAsVariant = material->property("controlPoints");
QQuick3DShaderUtilsTextureInput *splineInput
= splineInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
QQuick3DTexture *splineTexture = splineInput->texture();
QQuick3DTextureData *splineData = splineTexture->textureData();
bool loop = series->isSplineLooping();
material->setProperty("tension", series->splineTension());
material->setProperty("knotting", series->splineKnotting());
material->setProperty("loop", loop);
material->setProperty("color", series->splineColor());
const QScatterDataArray &array = series->dataArray();
qsizetype pointCount = array.size();
if (isDataDirty() && array.size() != 0) {
QVector<QVector4D> splinePoints;
QVector<SplineVertex> vertices;
splinePoints.reserve(pointCount + 2);
splineData->setSize(QSize(pointCount + 2, 1));
auto normalizedPos = [this](QVector3D pos) {
float posX = static_cast<QValue3DAxis *>(axisX())->positionAt(pos.x())
* scale().x()
+ translate().x();
float posY = static_cast<QValue3DAxis *>(axisY())->positionAt(pos.y())
* scale().y()
+ translate().y();
float posZ = static_cast<QValue3DAxis *>(axisZ())->positionAt(pos.z())
* scale().z()
+ translate().z();
return QVector3D(posX, posY, posZ);
};
QVector3D first = normalizedPos(array.at(0).position());
QVector3D second = normalizedPos(array.at(1).position());
QVector3D pStart = first + (first - second) * 0.1f;
QVector3D last = normalizedPos(array.at(pointCount - 1).position());
QVector3D secondLast = normalizedPos(array.at(pointCount - 2).position());
QVector3D pEnd = last + (last - secondLast) * 0.1f;
if (loop)
splinePoints.append(QVector4D(last, 1));
else
splinePoints.append(QVector4D(pStart, 1));
const qsizetype resolution = series->splineResolution();
vertices.reserve(resolution * pointCount);
for (int i = 0; i < pointCount; i++) {
splinePoints.push_back(QVector4D(normalizedPos(array.at(i).position()), 1));
for (int j = 0; j < resolution; j++) {
SplineVertex vertex;
vertex.position = QVector3D(float(j) / float(resolution), float(i), 0);
vertex.uv = QVector2D(float(j) / float(resolution - 1),
float(i) / float(pointCount + 2));
vertices.push_back(vertex);
}
}
if (loop)
splinePoints.append(QVector4D(first, 1));
else
splinePoints.append(QVector4D(pEnd, 1));
QByteArray pointData = QByteArray(reinterpret_cast<char *>(splinePoints.data()),
splinePoints.size() * sizeof(QVector4D));
splineData->setTextureData(pointData);
material->setProperty("points", splinePoints.size());
QQuick3DGeometry *splineGeometry = model->splineModel->geometry();
QByteArray vertexBuffer(reinterpret_cast<char *>(vertices.data()),
vertices.size() * sizeof(SplineVertex));
splineGeometry->setVertexData(vertexBuffer);
splineGeometry->update();
splineTexture->setTextureData(splineData);
splineInput->setTexture(splineTexture);
}
model->splineModel->setVisible(true);
}
}
}
void QQuickGraphsScatter::createSplineModel(ScatterModel *model)
{
QQuick3DModel *splineModel = new QQuick3DModel();
splineModel->setParent(model->series);
splineModel->setParentItem(graphNode());
splineModel->setObjectName(QStringLiteral("SplineModel"));
splineModel->setVisible(true);
splineModel->setPickable(false);
auto geometry = new QQuick3DGeometry();
geometry->setParent(splineModel);
geometry->setStride(sizeof(SplineVertex)); //pos + uv
geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::LineStrip);
geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
0,
QQuick3DGeometry::Attribute::F32Type);
geometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
sizeof(QVector3D),
QQuick3DGeometry::Attribute::F32Type);
splineModel->setGeometry(geometry);
QQuick3DTexture *splineTex = new QQuick3DTexture();
splineTex->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
splineTex->setVerticalTiling(QQuick3DTexture::ClampToEdge);
splineTex->setMinFilter(QQuick3DTexture::Nearest);
splineTex->setMagFilter(QQuick3DTexture::Nearest);
QQuick3DTextureData *splineData = new QQuick3DTextureData;
splineData->setSize(QSize(0, 1));
splineData->setFormat(QQuick3DTextureData::RGBA32F);
splineData->setParent(splineTex);
splineData->setParentItem(splineTex);
splineTex->setTextureData(splineData);
QQmlListReference materialRef(splineModel, "materials");
QQuick3DCustomMaterial *material = createQmlCustomMaterial(
QStringLiteral(":/materials/SplineMaterial"));
material->setParent(splineModel);
material->setParentItem(splineModel);
material->setObjectName("splineMaterial");
QVariant textureInputAsVariant = material->property("controlPoints");
QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant
.value<QQuick3DShaderUtilsTextureInput *>();
textureInput->setTexture(splineTex);
splineTex->setParent(material);
materialRef.append(material);
model->splineModel = splineModel;
if (auto series = qobject_cast<QSpline3DSeries *>(model->series)) {
connect(series,
&QSpline3DSeries::splineTensionChanged,
this,
&QQuickGraphsScatter::handleSplineChanged);
connect(series,
&QSpline3DSeries::splineKnottingChanged,
this,
&QQuickGraphsScatter::handleSplineChanged);
connect(series,
&QSpline3DSeries::splineLoopingChanged,
this,
&QQuickGraphsScatter::handleSplineChanged);
connect(series,
&QSpline3DSeries::splineColorChanged,
this,
&QQuickGraphsScatter::handleSplineChanged);
connect(series,
&QSpline3DSeries::splineResolutionChanged,
this,
&QQuickGraphsScatter::handleSplineChanged);
}
}
void QQuickGraphsScatter::handleSplineChanged()
{
m_isDataDirty = true;
}
QQuick3DModel *QQuickGraphsScatter::selected() const
{
return m_selected;
@ -1640,8 +1813,10 @@ void QQuickGraphsScatter::updateGraph()
}
}
if (isDataDirty() || isSeriesVisualsDirty())
if (isDataDirty() || isSeriesVisualsDirty()) {
updateScatterGraphItemPositions(graphModel);
updateSpline(graphModel);
}
if (isSeriesVisualsDirty() || (graphModel->instancing && graphModel->instancing->isDirty()))
updateScatterGraphItemVisuals(graphModel);

View File

@ -14,6 +14,7 @@
#include "qquickgraphsitem_p.h"
#include "qscatter3dseries.h"
#include "qspline3dseries.h"
#include "qvalue3daxis.h"
#include <private/scatterinstancing_p.h>
@ -148,6 +149,12 @@ private:
QList<InsertRemoveRecord> m_insertRemoveRecords;
bool m_recordInsertsAndRemoves;
struct SplineVertex
{
QVector3D position;
QVector2D uv;
};
struct ScatterModel
{
QList<QQuick3DModel *> dataItems;
@ -162,6 +169,8 @@ private:
ScatterInstancing *instancing = nullptr;
QQuick3DModel *instancingRootItem = nullptr;
QQuick3DModel *selectionIndicator = nullptr;
QQuick3DModel *splineModel = nullptr;
};
float m_maxItemSize = 0.0f;
@ -235,6 +244,10 @@ private:
void updatePointScaleSize();
void calculatePolarXZ(const float posX, const float posZ, float &x, float &z) const;
void updateSpline(ScatterModel *model);
void createSplineModel(ScatterModel *model);
void handleSplineChanged();
void generatePointsForScatterModel(ScatterModel *series);
void updateScatterGraphItemPositions(ScatterModel *graphModel);
void updateScatterGraphItemVisuals(ScatterModel *graphModel);

View File

@ -0,0 +1,133 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtCore/QMetaMethod>
#include "qquickgraphssplineseries_p.h"
#include "utils_p.h"
QT_BEGIN_NAMESPACE
QQuickGraphsSpline3DSeries::QQuickGraphsSpline3DSeries(QObject *parent)
: QSpline3DSeries(parent)
{}
QQuickGraphsSpline3DSeries::~QQuickGraphsSpline3DSeries() {}
QQmlListProperty<QObject> QQuickGraphsSpline3DSeries::seriesChildren()
{
return QQmlListProperty<QObject>(this,
this,
&QQuickGraphsSpline3DSeries::appendSeriesChildren,
0,
0,
0);
}
void QQuickGraphsSpline3DSeries::appendSeriesChildren(QQmlListProperty<QObject> *list,
QObject *element)
{
QScatterDataProxy *proxy = qobject_cast<QScatterDataProxy *>(element);
if (proxy)
reinterpret_cast<QQuickGraphsSpline3DSeries *>(list->data)->setDataProxy(proxy);
}
void QQuickGraphsSpline3DSeries::setBaseGradient(QQuickGradient *gradient)
{
if (m_baseGradient != gradient) {
setGradientHelper(gradient, m_baseGradient, GradientType::Base);
m_baseGradient = gradient;
Q_EMIT baseGradientChanged(m_baseGradient);
}
}
QQuickGradient *QQuickGraphsSpline3DSeries::baseGradient() const
{
return m_baseGradient;
}
void QQuickGraphsSpline3DSeries::setSingleHighlightGradient(QQuickGradient *gradient)
{
if (m_singleHighlightGradient != gradient) {
setGradientHelper(gradient, m_singleHighlightGradient, GradientType::Single);
m_singleHighlightGradient = gradient;
Q_EMIT singleHighlightGradientChanged(m_singleHighlightGradient);
}
}
QQuickGradient *QQuickGraphsSpline3DSeries::singleHighlightGradient() const
{
return m_singleHighlightGradient;
}
void QQuickGraphsSpline3DSeries::setMultiHighlightGradient(QQuickGradient *gradient)
{
if (m_multiHighlightGradient != gradient) {
setGradientHelper(gradient, m_multiHighlightGradient, GradientType::Multi);
m_multiHighlightGradient = gradient;
Q_EMIT multiHighlightGradientChanged(m_multiHighlightGradient);
}
}
QQuickGradient *QQuickGraphsSpline3DSeries::multiHighlightGradient() const
{
return m_multiHighlightGradient;
}
int QQuickGraphsSpline3DSeries::invalidSelectionIndex() const
{
return QSpline3DSeries::invalidSelectionIndex();
}
void QQuickGraphsSpline3DSeries::handleBaseGradientUpdate()
{
if (!m_baseGradient)
Utils::setSeriesGradient(this, m_baseGradient, GradientType::Base);
}
void QQuickGraphsSpline3DSeries::handleSingleHighlightGradientUpdate()
{
if (!m_singleHighlightGradient)
Utils::setSeriesGradient(this, m_singleHighlightGradient, GradientType::Single);
}
void QQuickGraphsSpline3DSeries::handleMultiHighlightGradientUpdate()
{
if (!m_multiHighlightGradient)
Utils::setSeriesGradient(this, m_multiHighlightGradient, GradientType::Multi);
}
void QQuickGraphsSpline3DSeries::setGradientHelper(QQuickGradient *newGradient,
QQuickGradient *memberGradient,
GradientType type)
{
if (memberGradient)
QObject::disconnect(memberGradient, 0, this, 0);
Utils::setSeriesGradient(this, newGradient, type);
memberGradient = newGradient;
if (memberGradient) {
switch (type) {
case GradientType::Base:
QObject::connect(memberGradient,
&QQuickGradient::updated,
this,
&QQuickGraphsSpline3DSeries::handleBaseGradientUpdate);
break;
case GradientType::Single:
QObject::connect(memberGradient,
&QQuickGradient::updated,
this,
&QQuickGraphsSpline3DSeries::handleSingleHighlightGradientUpdate);
break;
case GradientType::Multi:
QObject::connect(memberGradient,
&QQuickGradient::updated,
this,
&QQuickGraphsSpline3DSeries::handleMultiHighlightGradientUpdate);
break;
default:
break;
}
}
}
QT_END_NAMESPACE

View File

@ -0,0 +1,81 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
//
// W A R N I N G
// -------------
//
// This file is not part of the QtGraphs API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
#ifndef QQUICKGRAPHSSPLINESERIES_P_H
#define QQUICKGRAPHSSPLINESERIES_P_H
#include "theme/qquickgraphscolor_p.h"
#include <qspline3dseries.h>
#include <QtQml/qqml.h>
#include <QtQuick/private/qquickrectangle_p.h>
#include <private/qgraphsglobal_p.h>
QT_BEGIN_NAMESPACE
class QQuickGraphsSpline3DSeries : public QSpline3DSeries
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<QObject> seriesChildren READ seriesChildren CONSTANT)
Q_PROPERTY(QQuickGradient *baseGradient READ baseGradient WRITE setBaseGradient NOTIFY
baseGradientChanged FINAL)
Q_PROPERTY(QQuickGradient *singleHighlightGradient READ singleHighlightGradient WRITE
setSingleHighlightGradient NOTIFY singleHighlightGradientChanged FINAL)
Q_PROPERTY(QQuickGradient *multiHighlightGradient READ multiHighlightGradient WRITE
setMultiHighlightGradient NOTIFY multiHighlightGradientChanged FINAL)
// This is static method in parent class, overload as constant property for qml.
Q_PROPERTY(int invalidSelectionIndex READ invalidSelectionIndex CONSTANT)
Q_CLASSINFO("DefaultProperty", "seriesChildren")
QML_ADDED_IN_VERSION(6, 9)
QML_NAMED_ELEMENT(Spline3DSeries)
public:
QQuickGraphsSpline3DSeries(QObject *parent = 0);
~QQuickGraphsSpline3DSeries() override;
QQmlListProperty<QObject> seriesChildren();
static void appendSeriesChildren(QQmlListProperty<QObject> *list, QObject *element);
void setBaseGradient(QQuickGradient *gradient);
QQuickGradient *baseGradient() const;
void setSingleHighlightGradient(QQuickGradient *gradient);
QQuickGradient *singleHighlightGradient() const;
void setMultiHighlightGradient(QQuickGradient *gradient);
QQuickGradient *multiHighlightGradient() const;
int invalidSelectionIndex() const;
public Q_SLOTS:
void handleBaseGradientUpdate();
void handleSingleHighlightGradientUpdate();
void handleMultiHighlightGradientUpdate();
Q_SIGNALS:
void baseGradientChanged(QQuickGradient *gradient);
void singleHighlightGradientChanged(QQuickGradient *gradient);
void multiHighlightGradientChanged(QQuickGradient *gradient);
private:
QQuickGradient *m_baseGradient = nullptr;
QQuickGradient *m_singleHighlightGradient = nullptr;
QQuickGradient *m_multiHighlightGradient = nullptr;
void setGradientHelper(QQuickGradient *newGradient,
QQuickGradient *memberGradient,
GradientType type);
};
QT_END_NAMESPACE
#endif

View File

@ -0,0 +1,20 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick3D
import QtQuick
CustomMaterial {
property TextureInput controlPoints: TextureInput {}
property int points
property real tension
property real knotting
property bool loop
property color color
shadingMode: CustomMaterial.Shaded
vertexShader: "qrc:/shaders/splinevert"
fragmentShader: "qrc:/shaders/splinefrag"
}

View File

@ -14,6 +14,7 @@ if(QT_FEATURE_graphs_3d_scatter3d)
add_subdirectory(qgscatter-proxy)
add_subdirectory(qgscatter-modelproxy)
add_subdirectory(qgscatter-series)
add_subdirectory(qgspline-series)
endif()
if(QT_FEATURE_graphs_3d_surface3d)
add_subdirectory(qgsurface)

View File

@ -0,0 +1,14 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
qt_internal_add_test(tst_qgspline_series
SOURCES
tst_series.cpp
INCLUDE_DIRECTORIES
../common
LIBRARIES
Qt::Gui
Qt::GuiPrivate
Qt::Graphs
Qt::GraphsWidgets
)

View File

@ -0,0 +1,114 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QtTest>
#include <QtGraphs/QSpline3DSeries>
class tst_series : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void init();
void cleanup();
void construct();
void initialProperties();
void initializeProperties();
private:
QSpline3DSeries *m_series;
};
void tst_series::initTestCase() {}
void tst_series::cleanupTestCase() {}
void tst_series::init()
{
m_series = new QSpline3DSeries();
}
void tst_series::cleanup()
{
delete m_series;
}
void tst_series::construct()
{
QSpline3DSeries *series = new QSpline3DSeries();
QVERIFY(series);
delete series;
QScatterDataProxy *proxy = new QScatterDataProxy();
series = new QSpline3DSeries(proxy);
QVERIFY(series);
QCOMPARE(series->dataProxy(), proxy);
delete series;
}
void tst_series::initialProperties()
{
QVERIFY(m_series);
//Scatter properties
QVERIFY(m_series->dataProxy());
QCOMPARE(m_series->itemSize(), 0.0f);
QCOMPARE(m_series->selectedItem(), m_series->invalidSelectionIndex());
// Common properties. The ones identical between different series are tested in QBar3DSeries tests
QCOMPARE(m_series->itemLabelFormat(), QString("@xLabel, @yLabel, @zLabel"));
QCOMPARE(m_series->mesh(), QAbstract3DSeries::Mesh::Sphere);
QCOMPARE(m_series->type(), QAbstract3DSeries::SeriesType::Scatter);
//Spline properties
QCOMPARE(m_series->isSplineVisible(), true);
QCOMPARE(m_series->isSplineLooping(), false);
QCOMPARE(m_series->splineTension(), 0.0f);
QCOMPARE(m_series->splineKnotting(), 0.5f);
QCOMPARE(m_series->splineColor(), QColor(255, 0, 0));
QCOMPARE(m_series->splineResolution(), 10);
}
void tst_series::initializeProperties()
{
QVERIFY(m_series);
//Scatter properties
m_series->setDataProxy(new QScatterDataProxy());
m_series->setItemSize(0.5f);
m_series->setSelectedItem(0);
QCOMPARE(m_series->itemSize(), 0.5f);
QCOMPARE(m_series->selectedItem(), 0);
// Common properties. The ones identical between different series are tested in QBar3DSeries tests
m_series->setMesh(QAbstract3DSeries::Mesh::Point);
m_series->setMeshRotation(QQuaternion(1, 1, 10, 20));
QCOMPARE(m_series->mesh(), QAbstract3DSeries::Mesh::Point);
QCOMPARE(m_series->meshRotation(), QQuaternion(1, 1, 10, 20));
//spline properties
m_series->setSplineVisible(false);
m_series->setSplineLooping(true);
m_series->setSplineTension(1.0f);
m_series->setSplineKnotting(1.0f);
m_series->setSplineColor(QColor(0, 255, 0));
m_series->setSplineResolution(5);
QCOMPARE(m_series->isSplineVisible(), false);
QCOMPARE(m_series->isSplineLooping(), true);
QCOMPARE(m_series->splineTension(), 1.0f);
QCOMPARE(m_series->splineKnotting(), 1.0f);
QCOMPARE(m_series->splineColor(), QColor(0, 255, 0));
QCOMPARE(m_series->splineResolution(), 5);
}
QTEST_MAIN(tst_series)
#include "tst_series.moc"

View File

@ -0,0 +1,265 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick 2.0
import QtGraphs
import QtTest 1.0
Item {
id: top
height: 150
width: 150
Spline3DSeries {
id: initial
}
Gradient {
id: gradient1;
stops: [
GradientStop { color: "red"; position: 0 },
GradientStop { color: "blue"; position: 1 }
]
}
Gradient {
id: gradient2;
stops: [
GradientStop { color: "green"; position: 0 },
GradientStop { color: "red"; position: 1 }
]
}
Gradient {
id: gradient3;
stops: [
GradientStop { color: "gray"; position: 0 },
GradientStop { color: "darkgray"; position: 1 }
]
}
Spline3DSeries {
id: initialized
dataProxy: ItemModelScatterDataProxy {
itemModel: ListModel {
ListElement{ xPos: "2.754"; yPos: "1.455"; zPos: "3.362"; }
ListElement{ xPos: "3.164"; yPos: "2.022"; zPos: "4.348"; }
}
xPosRole: "xPos"
yPosRole: "yPos"
zPosRole: "zPos"
}
itemSize: 0.5
selectedItem: 0
baseColor: "blue"
baseGradient: gradient1
colorStyle: GraphsTheme.ColorStyle.ObjectGradient
itemLabelFormat: "%f"
itemLabelVisible: false
mesh: Abstract3DSeries.Mesh.Minimal
meshRotation: Qt.quaternion(1, 1, 1, 1)
meshSmooth: true
multiHighlightColor: "green"
multiHighlightGradient: gradient2
name: "series1"
singleHighlightColor: "red"
singleHighlightGradient: gradient3
userDefinedMesh: ":/customitem.obj"
visible: false
//spline properties
splineVisible: false
splineLooping: true
splineTension: 1.0
splineKnotting: 1.0
splineColor: "blue"
splineResolution: 5
}
ItemModelScatterDataProxy {
id: proxy1
itemModel: ListModel {
ListElement{ xPos: "2.754"; yPos: "1.455"; zPos: "3.362"; }
ListElement{ xPos: "3.164"; yPos: "2.022"; zPos: "4.348"; }
ListElement{ xPos: "4.564"; yPos: "1.865"; zPos: "1.346"; }
}
xPosRole: "xPos"
yPosRole: "yPos"
zPosRole: "zPos"
}
Spline3DSeries {
id: change
dataProxy: proxy1
}
Spline3DSeries {
id: invalid
}
TestCase {
name: "Spline3DSeries Initial"
function test_1_initial() {
compare(initial.dataProxy.itemCount, 0)
compare(initial.invalidSelectionIndex, -1)
compare(initial.itemSize, 0.0)
compare(initial.selectedItem, -1)
//Spline properties
compare(initial.splineVisible, true)
compare(initial.splineLooping, false)
compare(initial.splineTension, 0.0)
compare(initial.splineKnotting, 0.5)
compare(initial.splineColor, "#ff0000")
compare(initial.splineResolution, 10)
}
function test_2_initial_common() {
// Common properties
compare(initial.baseColor, "#000000")
verify(!initial.baseGradient)
compare(initial.colorStyle, GraphsTheme.ColorStyle.Uniform)
compare(initial.itemLabel, "")
compare(initial.itemLabelFormat, "@xLabel, @yLabel, @zLabel")
compare(initial.itemLabelVisible, true)
compare(initial.mesh, Abstract3DSeries.Mesh.Sphere)
compare(initial.meshRotation, Qt.quaternion(1, 0, 0, 0))
compare(initial.meshSmooth, false)
compare(initial.multiHighlightColor, "#000000")
verify(!initial.multiHighlightGradient)
compare(initial.name, "")
compare(initial.singleHighlightColor, "#000000")
verify(!initial.singleHighlightGradient)
compare(initial.type, Abstract3DSeries.SeriesType.Scatter)
compare(initial.userDefinedMesh, "")
compare(initial.visible, true)
}
}
TestCase {
name: "Spline3DSeries Initialized"
function test_1_initialized() {
compare(initialized.dataProxy.itemCount, 2)
compare(initialized.itemSize, 0.5)
compare(initialized.selectedItem, 0)
//spline properties
compare(initialized.splineVisible, false)
compare(initialized.splineLooping, true)
compare(initialized.splineTension, 1.0)
compare(initialized.splineKnotting, 1.0)
compare(initialized.splineColor, "#0000ff")
compare(initialized.splineResolution, 5)
}
function test_2_initialized_common() {
// Common properties
compare(initialized.baseColor, "#0000ff")
compare(initialized.baseGradient, gradient1)
compare(initialized.colorStyle, GraphsTheme.ColorStyle.ObjectGradient)
compare(initialized.itemLabelFormat, "%f")
compare(initialized.itemLabelVisible, false)
compare(initialized.mesh, Abstract3DSeries.Mesh.Minimal)
compare(initialized.meshRotation, Qt.quaternion(1, 1, 1, 1))
compare(initialized.meshSmooth, true)
compare(initialized.multiHighlightColor, "#008000")
compare(initialized.multiHighlightGradient, gradient2)
compare(initialized.name, "series1")
compare(initialized.singleHighlightColor, "#ff0000")
compare(initialized.singleHighlightGradient, gradient3)
compare(initialized.userDefinedMesh, ":/customitem.obj")
compare(initialized.visible, false)
}
}
TestCase {
name: "Spline3DSeries Change"
function test_1_change() {
change.itemSize = 0.5
change.selectedItem = 0
//spline properties
change.splineVisible = false
change.splineLooping = true
change.splineTension = 1.0
change.splineKnotting = 1.0
change.splineColor = "green"
change.splineResolution = 15
}
function test_2_test_change() {
// This test has a dependency to the previous one due to asynchronous item model resolving
compare(change.dataProxy.itemCount, 3)
compare(change.itemSize, 0.5)
compare(change.selectedItem, 0)
//spline properties
compare(change.splineVisible, false)
compare(change.splineLooping, true)
compare(change.splineTension, 1.0)
compare(change.splineKnotting, 1.0)
compare(change.splineColor, "#008000")
compare(change.splineResolution, 15)
}
function test_3_change_common() {
change.baseColor = "blue"
change.baseGradient = gradient1
change.colorStyle = GraphsTheme.ColorStyle.ObjectGradient
change.itemLabelFormat = "%f"
change.itemLabelVisible = false
change.mesh = Abstract3DSeries.Mesh.Minimal
change.meshRotation = Qt.quaternion(1, 1, 1, 1)
change.meshSmooth = true
change.multiHighlightColor = "green"
change.multiHighlightGradient = gradient2
change.name = "series1"
change.singleHighlightColor = "red"
change.singleHighlightGradient = gradient3
change.userDefinedMesh = ":/customitem.obj"
change.visible = false
compare(change.baseColor, "#0000ff")
compare(change.baseGradient, gradient1)
compare(change.colorStyle, GraphsTheme.ColorStyle.ObjectGradient)
compare(change.itemLabelFormat, "%f")
compare(change.itemLabelVisible, false)
compare(change.mesh, Abstract3DSeries.Mesh.Minimal)
compare(change.meshRotation, Qt.quaternion(1, 1, 1, 1))
compare(change.meshSmooth, true)
compare(change.multiHighlightColor, "#008000")
compare(change.multiHighlightGradient, gradient2)
compare(change.name, "series1")
compare(change.singleHighlightColor, "#ff0000")
compare(change.singleHighlightGradient, gradient3)
compare(change.userDefinedMesh, ":/customitem.obj")
compare(change.visible, false)
}
function test_4_change_gradient_stop() {
gradient1.stops[0].color = "yellow"
compare(change.baseGradient.stops[0].color, "#ffff00")
}
}
TestCase {
name: "Spline3DSeries Invalid"
function test_invalid() {
invalid.itemSize = -1.0
compare(invalid.itemSize, 0.0)
invalid.itemSize = 1.1
compare(invalid.itemSize, 0.0)
invalid.splineTension = -1.0
compare(invalid.splineTension, 0.0)
invalid.splineKnotting = -1.0
compare(invalid.splineKnotting, 0.5)
invalid.splineResolution = 1
compare(invalid.splineResolution, 10)
}
}
}

View File

@ -29,6 +29,7 @@ if(QT_FEATURE_graphs_3d)
if(QT_FEATURE_graphs_3d_scatter3d)
add_subdirectory(qmlcustominput)
add_subdirectory(qmldynamicdata)
add_subdirectory(qmlspline)
endif()
if(QT_FEATURE_graphs_3d_surface3d)
add_subdirectory(qmlgradient)

View File

@ -0,0 +1,31 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
set(CMAKE_INCLUDE_CURRENT_DIR ON)
qt_internal_add_manual_test(tst_qmlspline
GUI
SOURCES
main.cpp
)
target_sources(tst_qmlspline
PRIVATE
splinegen.h splinegen.cpp
)
target_link_libraries(tst_qmlspline PUBLIC
Qt::Gui
Qt::Graphs
)
set(qmlspline_resource_files
"qml/qmlspline/main.qml"
)
qt_internal_add_resource(tst_qmlspline "qmlspline"
PREFIX
"/"
FILES
${qmlspline_resource_files}
)

View File

@ -0,0 +1,38 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "splinegen.h"
#include <QtCore/QDir>
#include <QtGui/QGuiApplication>
#include <QtQml/QQmlContext>
#include <QtQml/QQmlEngine>
#include <QtQuick/QQuickView>
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQuickView viewer;
// The following are needed to make examples run without having to install the
// module in desktop environments.
#ifdef Q_OS_WIN
QString extraImportPath(QStringLiteral("%1/../../../%2"));
#else
QString extraImportPath(QStringLiteral("%1/../../%2"));
#endif
viewer.engine()->addImportPath(extraImportPath.arg(
QGuiApplication::applicationDirPath(), QString::fromLatin1("qml")));
QObject::connect(viewer.engine(), &QQmlEngine::quit, &viewer,
&QWindow::close);
SplineGen splineGen;
viewer.rootContext()->setContextProperty("splineGen", &splineGen);
viewer.setTitle(QStringLiteral("QML Scatter spline"));
viewer.setSource(QUrl("qrc:/qml/qmlspline/main.qml"));
viewer.setResizeMode(QQuickView::SizeRootObjectToView);
viewer.show();
return app.exec();
}

View File

@ -0,0 +1,195 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import QtQuick3D.Helpers
import QtQuick.Dialogs
import QtGraphs
import "."
Item {
id: mainView
width: 1280
height: 1024
Scatter3D {
id: scatterGraph
property int splineType: 0
width: parent.width
height: parent.height
aspectRatio: 1
axisX.min: -1.2
axisX.max: 1.2
axisZ.min: -1.2
axisZ.max: 1.2
theme: GraphsTheme {
theme: GraphsTheme.Theme.QtGreen
colorStyle: GraphsTheme.ColorStyle.Uniform
}
Spline3DSeries {
id: splineSeries
splineVisible: true
splineTension: tensionSlider.value
splineKnotting: knottingSlider.value
splineLooping: looping.checked
splineResolution: resolution.value
itemSize: pointSizeSlider.value
baseColor: "white"
Component.onCompleted: splineGen.generateSpline(splineSeries, scatterGraph.splineType, points.value)
}
}
RowLayout {
id: settings
anchors.top: parent.top
anchors.left: parent.left
ComboBox {
id: splineTypeBox
model: ["Circle","Helix", "Stitch curve"]
onActivated: {
scatterGraph.splineType = currentIndex
splineGen.generateSpline(splineSeries, scatterGraph.splineType, points.value)
}
}
Button {
id: liveToggle
text: liveTimer.running? "Static" : "Live"
onClicked: liveTimer.running = !liveTimer.running
}
ColumnLayout {
Text {
text: qsTr("Number of points")
color: "white"
}
SpinBox {
id: points
from: 0
value: 64
to: 80
editable: true
onValueChanged: splineGen.generateSpline(splineSeries, scatterGraph.splineType, value);
}
}
ColumnLayout {
Text {
text: qsTr("Point size")
color: "white"
}
Slider {
id: pointSizeSlider
from: 0.001
to: 0.1
value: 0.001
}
}
ColumnLayout {
Text {
text: qsTr("Spline color")
color: "white"
}
Button{
height: 25
width: 25
onClicked: splineCol.open()
Rectangle {
anchors.fill: parent
anchors.margins: 5
color: splineSeries.splineColor
}
}
ColorDialog {
id: splineCol
selectedColor: splineSeries.splineColor
onAccepted: splineSeries.splineColor = selectedColor
}
}
ColumnLayout {
Text {
text: qsTr("Spline resolution")
color: "white"
}
SpinBox {
id: resolution
from: 2
value: 10
to: 30
editable: true
}
}
}
RowLayout {
id: splineSettings
anchors.top: settings.bottom
ColumnLayout {
Text {
id: tensionText
text: qsTr("Tension")
color: "white"
}
Slider {
id: tensionSlider
from: 0
to: 1
value: 0
stepSize: 0.1
}
}
ColumnLayout {
Text {
id: knottingText
color: "white"
text: qsTr("Knotting")
}
Slider {
id: knottingSlider
from: 0
to: 1
value: 0.5
stepSize: 0.1
}
}
ColumnLayout {
Text {
id: loopingText
text: qsTr("Looping")
color: "white"
}
CheckBox {
id: looping
checked: false
}
}
}
Timer {
id: liveTimer
interval: 1000 / 60
repeat: true
running: false
onTriggered: splineGen.tickSpline(splineSeries)
}
}

View File

@ -0,0 +1,133 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "splinegen.h"
SplineGen::SplineGen(QObject *parent)
: QObject(parent)
{
qRegisterMetaType<QScatter3DSeries *>();
}
SplineGen::~SplineGen()
{
m_series->deleteLater();
m_splineCache.clear();
}
void SplineGen::generateSpline(QScatter3DSeries *series, SplineType type, int points)
{
if (m_series != series)
m_series = series;
switch (type) {
case (SplineType::Circle):
genCircle(points);
break;
case (SplineType::Helix):
genHelix(points);
break;
case (SplineType::Stitch):
genStitch(points);
break;
}
series->dataProxy()->resetArray(m_splineCache.at(0));
}
void SplineGen::tickSpline(QScatter3DSeries *series)
{
if (!series || series->dataProxy()->itemCount() == 0)
return;
static int index = 0;
int points = series->dataProxy()->itemCount();
QScatterDataArray newArray;
newArray.reserve(points);
const QScatterDataArray &cache = m_splineCache.at(index);
for (int i = 0; i < points; i++) {
newArray.append(cache.at(i));
}
series->dataProxy()->resetArray(newArray);
index++;
if (index >= m_cacheCount)
index = 0;
}
void SplineGen::genCircle(int points)
{
for (int i = 0; i < m_splineCache.size(); i++) {
QScatterDataArray &array = m_splineCache[i];
array.clear();
}
m_splineCache.resize(m_cacheCount);
for (int i = 0; i < m_cacheCount; i++) {
QScatterDataArray &array = m_splineCache[i];
array.reserve(points);
float offset = 2 * M_PI * i / m_cacheCount;
for (int j = 0; j < points; j++) {
float t = 2 * M_PI * j / points;
float x = qCos(t + offset);
float y = 0.5;
float z = qSin(t + offset);
array.append(QScatterDataItem(x, y, z));
}
}
}
void SplineGen::genHelix(int points)
{
for (int i = 0; i < m_splineCache.size(); i++) {
QScatterDataArray &array = m_splineCache[i];
array.clear();
}
// create cache array;
m_splineCache.resize(m_cacheCount);
for (int i = 0; i < m_cacheCount; i++) {
QScatterDataArray &array = m_splineCache[i];
array.reserve(points);
float offset = 2 * M_PI * i / m_cacheCount;
for (int j = 0; j < points; j++) {
//8 points per revolution
float t = 4 * M_PI * j / points;
float x = qCos(t + offset);
float y = t;
float z = qSin(t + offset);
array.append(QScatterDataItem(x, y, z));
}
}
}
void SplineGen::genStitch(int points)
{
for (int i = 0; i < m_splineCache.size(); i++) {
QScatterDataArray &array = m_splineCache[i];
array.clear();
}
m_splineCache.resize(m_cacheCount);
for (int i = 0; i < m_cacheCount; i++) {
QScatterDataArray &array = m_splineCache[i];
float t = qSin(2 * M_PI * float(i) / float(m_cacheCount));
// round to closest fitting amount
int pointsPerAxis = int(qFloor(float(points) / 3.0));
int rounds = int(qCeil(float(pointsPerAxis) / 2.0));
array.reserve(pointsPerAxis * 3 + rounds);
const QVector3D masks[3] = {QVector3D(t, 1, 0), QVector3D(1, 0, t), QVector3D(0, t, 1)};
for (int j = 1; j <= rounds; j++) {
bool top = true;
for (int k = 0; k < 7; k++) {
float value = top ? pointsPerAxis + 1 - j : j;
value /= pointsPerAxis;
top = !top;
QVector3D p = masks[k % 3] * value;
array.append(QScatterDataItem(p));
}
}
}
}

View File

@ -0,0 +1,35 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef SPLINEGEN_H
#define SPLINEGEN_H
#include <QtGraphs>
enum class SplineType {
Circle,
Helix,
Stitch,
};
class SplineGen : public QObject
{
Q_OBJECT
Q_ENUM(SplineType)
public:
SplineGen(QObject *parent = 0);
~SplineGen() override;
public Q_SLOTS:
void generateSpline(QScatter3DSeries *series, SplineType type, int points);
void tickSpline(QScatter3DSeries *series);
private:
void genCircle(int points);
void genHelix(int points);
void genStitch(int points);
int m_cacheCount = 60;
QScatter3DSeries *m_series = nullptr;
QList<QScatterDataArray> m_splineCache;
};
#endif // SPLINEGEN_H