aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>2023-07-13 11:45:55 +0200
committerEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>2023-08-14 15:43:58 +0200
commit218507a8afe3a58b0249004a23e87c7abc207b5e (patch)
treedf48bee11ce7f390a5f3e33dcc6973b1e9974262
parentee6a616e48d526408962ab915e0fd144d8dd7495 (diff)
Improve Qt Quick Shapes curve renderer strokes
This adds a specialized stroke shader which replaces the curve flattening triangulating stroker as the default. The triangulating stroker is still available via an environment variable for testing. Dashed strokes are now done by cutting the path into segments ahead of time and applying the normal solid stroking algorithm to the corresponding path. In addition, the fill shader no longer depends on the derivatives extension except when used in 3D scenes to support perspective transforms. The preprocessing of the shape has also gotten a few bug fixes and improvements and there are extremely few artifacts left. Done-with: Paul Olav Tvete <paul.tvete@qt.io> Done-with: Eirik Aavitsland <eirik.aavitsland@qt.io> Done-with: Matthias Rauter <matthias.rauter@qt.io> Task-number: QTBUG-104122 Change-Id: Ib49b41019956be3e3cd509d1f3cef6842658aceb Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io> Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Matthias Rauter <matthias.rauter@qt.io> (cherry picked from commit 73324c379f8c638106203d679549e7785d16af38) Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
-rw-r--r--src/quickshapes/CMakeLists.txt46
-rw-r--r--src/quickshapes/qquickshape.cpp5
-rw-r--r--src/quickshapes/qquickshapecurvenode.cpp63
-rw-r--r--src/quickshapes/qquickshapecurvenode_p.cpp338
-rw-r--r--src/quickshapes/qquickshapecurvenode_p.h193
-rw-r--r--src/quickshapes/qquickshapecurvenode_p_p.h43
-rw-r--r--src/quickshapes/qquickshapecurverenderer.cpp1868
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p.h53
-rw-r--r--src/quickshapes/qquickshapestrokenode.cpp140
-rw-r--r--src/quickshapes/qquickshapestrokenode_p.cpp83
-rw-r--r--src/quickshapes/qquickshapestrokenode_p.h94
-rw-r--r--src/quickshapes/qquickshapestrokenode_p_p.h72
-rw-r--r--src/quickshapes/shaders_ng/shapecurve.frag67
-rw-r--r--src/quickshapes/shaders_ng/shapecurve.vert22
-rw-r--r--src/quickshapes/shaders_ng/shapestroke.frag129
-rw-r--r--src/quickshapes/shaders_ng/shapestroke.vert46
-rw-r--r--tests/manual/painterpathquickshape/CMakeLists.txt78
-rw-r--r--tests/manual/painterpathquickshape/ControlPanel.qml41
-rw-r--r--tests/manual/painterpathquickshape/ControlledShape.qml8
-rw-r--r--tests/manual/painterpathquickshape/DashedStroke.qml24
-rw-r--r--tests/manual/painterpathquickshape/SimpleShape.qml9
-rw-r--r--tests/manual/painterpathquickshape/SvgShape.qml9
-rw-r--r--tests/manual/painterpathquickshape/main.cpp1
-rw-r--r--tests/manual/painterpathquickshape/main.qml83
-rw-r--r--tests/manual/painterpathquickshape/zoombox.frag22
25 files changed, 2216 insertions, 1321 deletions
diff --git a/src/quickshapes/CMakeLists.txt b/src/quickshapes/CMakeLists.txt
index f5697f974b..a988a535d8 100644
--- a/src/quickshapes/CMakeLists.txt
+++ b/src/quickshapes/CMakeLists.txt
@@ -21,6 +21,8 @@ qt_internal_add_qml_module(QuickShapesPrivate
qquickshapegenericrenderer.cpp qquickshapegenericrenderer_p.h
qquickshapesglobal.h qquickshapesglobal_p.h
qquickshapecurverenderer.cpp qquickshapecurverenderer_p.h qquickshapecurverenderer_p_p.h
+ qquickshapecurvenode.cpp qquickshapecurvenode_p.h qquickshapecurvenode_p_p.h qquickshapecurvenode_p.cpp
+ qquickshapestrokenode.cpp qquickshapestrokenode_p.h qquickshapestrokenode_p_p.h qquickshapestrokenode_p.cpp
qt_quadratic_bezier.cpp
qquickshapesoftwarerenderer.cpp qquickshapesoftwarerenderer_p.h
PUBLIC_LIBRARIES
@@ -56,24 +58,26 @@ qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders"
"shaders_ng/conicalgradient.frag"
"shaders_ng/shapecurve.frag"
"shaders_ng/shapecurve.vert"
+ "shaders_ng/shapestroke.frag"
+ "shaders_ng/shapestroke.vert"
"shaders_ng/wireframe.frag"
"shaders_ng/wireframe.vert"
)
-qt_internal_add_shaders(QuickShapesPrivate "shaders_stroke"
+qt_internal_add_shaders(QuickShapesPrivate "shaders_derivatives"
SILENT
BATCHABLE
PRECOMPILE
OPTIMIZED
- DEFINES "STROKE"
+ DEFINES "USE_DERIVATIVES"
PREFIX
"/qt-project.org/shapes"
FILES
"shaders_ng/shapecurve.frag"
"shaders_ng/shapecurve.vert"
OUTPUTS
- "shaders_ng/shapecurve_stroke.frag.qsb"
- "shaders_ng/shapecurve_stroke.vert.qsb"
+ "shaders_ng/shapecurve_derivatives.frag.qsb"
+ "shaders_ng/shapecurve_derivatives.vert.qsb"
)
qt_internal_add_shaders(QuickShapesPrivate "shaders_lg"
@@ -81,7 +85,8 @@ qt_internal_add_shaders(QuickShapesPrivate "shaders_lg"
BATCHABLE
PRECOMPILE
OPTIMIZED
- DEFINES "LINEARGRADIENT"
+ DEFINES
+ "LINEARGRADIENT"
PREFIX
"/qt-project.org/shapes"
FILES
@@ -92,22 +97,21 @@ qt_internal_add_shaders(QuickShapesPrivate "shaders_lg"
"shaders_ng/shapecurve_lg.vert.qsb"
)
-qt_internal_add_shaders(QuickShapesPrivate "shaders_lg_stroke"
- SILENT
+qt_internal_add_shaders(QuickShapesPrivate "shaders_lg_derivatives"
BATCHABLE
PRECOMPILE
OPTIMIZED
DEFINES
"LINEARGRADIENT"
- "STROKE"
+ "USE_DERIVATIVES"
PREFIX
"/qt-project.org/shapes"
FILES
"shaders_ng/shapecurve.frag"
"shaders_ng/shapecurve.vert"
OUTPUTS
- "shaders_ng/shapecurve_lg_stroke.frag.qsb"
- "shaders_ng/shapecurve_lg_stroke.vert.qsb"
+ "shaders_ng/shapecurve_lg_derivatives.frag.qsb"
+ "shaders_ng/shapecurve_lg_derivatives.vert.qsb"
)
qt_internal_add_shaders(QuickShapesPrivate "shaders_rg"
@@ -115,7 +119,8 @@ qt_internal_add_shaders(QuickShapesPrivate "shaders_rg"
BATCHABLE
PRECOMPILE
OPTIMIZED
- DEFINES "RADIALGRADIENT"
+ DEFINES
+ "RADIALGRADIENT"
PREFIX
"/qt-project.org/shapes"
FILES
@@ -126,22 +131,21 @@ qt_internal_add_shaders(QuickShapesPrivate "shaders_rg"
"shaders_ng/shapecurve_rg.vert.qsb"
)
-qt_internal_add_shaders(QuickShapesPrivate "shaders_rg_stroke"
- SILENT
+qt_internal_add_shaders(QuickShapesPrivate "shaders_rg_derivatives"
BATCHABLE
PRECOMPILE
OPTIMIZED
DEFINES
"RADIALGRADIENT"
- "STROKE"
+ "USE_DERIVATIVES"
PREFIX
"/qt-project.org/shapes"
FILES
"shaders_ng/shapecurve.frag"
"shaders_ng/shapecurve.vert"
OUTPUTS
- "shaders_ng/shapecurve_rg_stroke.frag.qsb"
- "shaders_ng/shapecurve_rg_stroke.vert.qsb"
+ "shaders_ng/shapecurve_rg_derivatives.frag.qsb"
+ "shaders_ng/shapecurve_rg_derivatives.vert.qsb"
)
qt_internal_add_shaders(QuickShapesPrivate "shaders_cg"
@@ -161,20 +165,20 @@ qt_internal_add_shaders(QuickShapesPrivate "shaders_cg"
"shaders_ng/shapecurve_cg.vert.qsb"
)
-qt_internal_add_shaders(QuickShapesPrivate "shaders_cg_stroke"
- SILENT
+qt_internal_add_shaders(QuickShapesPrivate "shaders_cg_derivatives"
BATCHABLE
PRECOMPILE
OPTIMIZED
DEFINES
"CONICALGRADIENT"
- "STROKE"
+ "USE_DERIVATIVES"
PREFIX
"/qt-project.org/shapes"
FILES
"shaders_ng/shapecurve.frag"
"shaders_ng/shapecurve.vert"
OUTPUTS
- "shaders_ng/shapecurve_cg_stroke.frag.qsb"
- "shaders_ng/shapecurve_cg_stroke.vert.qsb"
+ "shaders_ng/shapecurve_cg_derivatives.frag.qsb"
+ "shaders_ng/shapecurve_cg_derivatives.vert.qsb"
)
+
diff --git a/src/quickshapes/qquickshape.cpp b/src/quickshapes/qquickshape.cpp
index 273945e629..4f6282675b 100644
--- a/src/quickshapes/qquickshape.cpp
+++ b/src/quickshapes/qquickshape.cpp
@@ -1143,9 +1143,8 @@ QSGNode *QQuickShapePrivate::createNode()
default:
if (QSGRendererInterface::isApiRhiBased(ri->graphicsApi())) {
if (rendererType == QQuickShape::CurveRenderer) {
- node = new QQuickShapeCurveNode;
- static_cast<QQuickShapeCurveRenderer *>(renderer)->setRootNode(
- static_cast<QQuickShapeCurveNode *>(node));
+ node = new QSGNode;
+ static_cast<QQuickShapeCurveRenderer *>(renderer)->setRootNode(node);
} else {
node = new QQuickShapeGenericNode;
static_cast<QQuickShapeGenericRenderer *>(renderer)->setRootNode(
diff --git a/src/quickshapes/qquickshapecurvenode.cpp b/src/quickshapes/qquickshapecurvenode.cpp
new file mode 100644
index 0000000000..06fd3b815e
--- /dev/null
+++ b/src/quickshapes/qquickshapecurvenode.cpp
@@ -0,0 +1,63 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquickshapecurvenode_p.h"
+#include "qquickshapecurvenode_p_p.h"
+
+QT_BEGIN_NAMESPACE
+
+namespace {
+}
+
+QQuickShapeCurveNode::QQuickShapeCurveNode()
+{
+ setFlag(OwnsGeometry, true);
+ setGeometry(new QSGGeometry(attributes(), 0, 0));
+
+ updateMaterial();
+}
+
+void QQuickShapeCurveNode::updateMaterial()
+{
+ m_material.reset(new QQuickShapeCurveMaterial(this));
+ setMaterial(m_material.data());
+}
+
+void QQuickShapeCurveNode::cookGeometry()
+{
+ QSGGeometry *g = geometry();
+ if (g->indexType() != QSGGeometry::UnsignedIntType) {
+ g = new QSGGeometry(attributes(),
+ m_uncookedVertexes.size(),
+ m_uncookedIndexes.size(),
+ QSGGeometry::UnsignedIntType);
+ setGeometry(g);
+ } else {
+ g->allocate(m_uncookedVertexes.size(), m_uncookedIndexes.size());
+ }
+
+ g->setDrawingMode(QSGGeometry::DrawTriangles);
+ memcpy(g->vertexData(),
+ m_uncookedVertexes.constData(),
+ g->vertexCount() * g->sizeOfVertex());
+ memcpy(g->indexData(),
+ m_uncookedIndexes.constData(),
+ g->indexCount() * g->sizeOfIndex());
+
+ m_uncookedIndexes.clear();
+ m_uncookedVertexes.clear();
+}
+
+const QSGGeometry::AttributeSet &QQuickShapeCurveNode::attributes()
+{
+ static QSGGeometry::Attribute data[] = {
+ QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute),
+ QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
+ QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::ColorAttribute),
+ QSGGeometry::Attribute::createWithAttributeType(3, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute),
+ };
+ static QSGGeometry::AttributeSet attrs = { 4, sizeof(CurveNodeVertex), data };
+ return attrs;
+}
+
+QT_END_NAMESPACE
diff --git a/src/quickshapes/qquickshapecurvenode_p.cpp b/src/quickshapes/qquickshapecurvenode_p.cpp
new file mode 100644
index 0000000000..19f2a22887
--- /dev/null
+++ b/src/quickshapes/qquickshapecurvenode_p.cpp
@@ -0,0 +1,338 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquickshapecurvenode_p_p.h"
+#include "qquickshapecurvenode_p.h"
+
+#include "qquickshapegenericrenderer_p.h"
+
+QT_BEGIN_NAMESPACE
+
+namespace {
+
+ class QQuickShapeCurveMaterialShader : public QSGMaterialShader
+ {
+ public:
+ QQuickShapeCurveMaterialShader(QQuickAbstractPathRenderer::FillGradientType gradientType,
+ bool includeStroke,
+ bool useDerivatives);
+
+ bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override;
+ void updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
+ QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+ };
+
+ QQuickShapeCurveMaterialShader::QQuickShapeCurveMaterialShader(QQuickAbstractPathRenderer::FillGradientType gradientType,
+ bool includeStroke,
+ bool useDerivatives)
+ {
+ QString baseName = QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapecurve");
+
+ if (gradientType == QQuickAbstractPathRenderer::LinearGradient) {
+ baseName += QStringLiteral("_lg");
+ } else if (gradientType == QQuickAbstractPathRenderer::RadialGradient) {
+ baseName += QStringLiteral("_rg");
+ } else if (gradientType == QQuickAbstractPathRenderer::ConicalGradient) {
+ baseName += QStringLiteral("_cg");
+ }
+
+ if (includeStroke)
+ baseName += QStringLiteral("_stroke");
+
+ if (useDerivatives)
+ baseName += QStringLiteral("_derivatives");
+
+ setShaderFileName(VertexStage, baseName + QStringLiteral(".vert.qsb"));
+ setShaderFileName(FragmentStage, baseName + QStringLiteral(".frag.qsb"));
+ }
+
+ void QQuickShapeCurveMaterialShader::updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
+ QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+ {
+ Q_UNUSED(oldMaterial);
+ const QQuickShapeCurveMaterial *m = static_cast<QQuickShapeCurveMaterial *>(newMaterial);
+ const QQuickShapeCurveNode *node = m->node();
+ if (binding != 1 || node->gradientType() == QQuickAbstractPathRenderer::NoGradient)
+ return;
+
+ const QQuickShapeGradientCacheKey cacheKey(node->fillGradient().stops,
+ node->fillGradient().spread);
+ QSGTexture *t = QQuickShapeGradientCache::cacheForRhi(state.rhi())->get(cacheKey);
+ t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());
+ *texture = t;
+ }
+
+ bool QQuickShapeCurveMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect)
+ {
+ bool changed = false;
+ QByteArray *buf = state.uniformData();
+ Q_ASSERT(buf->size() >= 80);
+
+ int offset = 0;
+ float matrixScale = 0.0f;
+ if (state.isMatrixDirty()) {
+ const QMatrix4x4 m = state.combinedMatrix();
+
+ memcpy(buf->data() + offset, m.constData(), 64);
+ offset += 64;
+
+ matrixScale = qSqrt(qAbs(state.determinant()));
+ memcpy(buf->data() + offset, &matrixScale, 4);
+ offset += 4;
+
+ changed = true;
+ }
+
+ if (state.isOpacityDirty()) {
+ const float opacity = state.opacity();
+ memcpy(buf->data() + offset, &opacity, 4);
+ changed = true;
+ }
+ offset += 12;
+
+ QQuickShapeCurveMaterial *newMaterial = static_cast<QQuickShapeCurveMaterial *>(newEffect);
+ QQuickShapeCurveMaterial *oldMaterial = static_cast<QQuickShapeCurveMaterial *>(oldEffect);
+
+ QQuickShapeCurveNode *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr;
+ QQuickShapeCurveNode *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr;
+
+ if (newNode == nullptr)
+ return changed;
+
+ if (newNode->hasStroke()) {
+ Q_ASSERT(buf->size() >= offset + 32);
+ QVector4D newStrokeColor(newNode->strokeColor().redF(),
+ newNode->strokeColor().greenF(),
+ newNode->strokeColor().blueF(),
+ newNode->strokeColor().alphaF());
+ QVector4D oldStrokeColor = oldNode != nullptr
+ ? QVector4D(oldNode->strokeColor().redF(),
+ oldNode->strokeColor().greenF(),
+ oldNode->strokeColor().blueF(),
+ oldNode->strokeColor().alphaF())
+ : QVector4D{};
+
+ if (oldNode == nullptr || oldStrokeColor != newStrokeColor) {
+ memcpy(buf->data() + offset, &newStrokeColor, 16);
+ changed = true;
+ }
+ offset += 16;
+
+ if (oldNode == nullptr
+ || !qFuzzyCompare(newNode->strokeWidth(), oldNode->strokeWidth())
+ || (state.isMatrixDirty() && newNode->strokeWidth() > 0.0f)) {
+ float w = newNode->strokeWidth() * matrixScale; // matrixScale calculated earlier
+ memcpy(buf->data() + offset, &w, 4);
+ changed = true;
+ }
+ offset += 16;
+ }
+
+ if (newNode->gradientType() == QQuickAbstractPathRenderer::NoGradient) {
+ Q_ASSERT(buf->size() >= offset + 16);
+
+ QVector4D newColor = QVector4D(newNode->color().redF(),
+ newNode->color().greenF(),
+ newNode->color().blueF(),
+ newNode->color().alphaF());
+ QVector4D oldColor = oldNode != nullptr
+ ? QVector4D(oldNode->color().redF(),
+ oldNode->color().greenF(),
+ oldNode->color().blueF(),
+ oldNode->color().alphaF())
+ : QVector4D{};
+
+ if (oldNode == nullptr || oldColor != newColor) {
+ memcpy(buf->data() + offset, &newColor, 16);
+ changed = true;
+ }
+
+ offset += 16;
+ } else if (newNode->gradientType() == QQuickAbstractPathRenderer::LinearGradient) {
+ Q_ASSERT(buf->size() >= offset + 8 + 8);
+
+ QVector2D newGradientStart = QVector2D(newNode->fillGradient().a);
+ QVector2D oldGradientStart = oldNode != nullptr
+ ? QVector2D(oldNode->fillGradient().a)
+ : QVector2D{};
+
+ if (newGradientStart != oldGradientStart || oldEffect == nullptr) {
+ memcpy(buf->data() + offset, &newGradientStart, 8);
+ changed = true;
+ }
+ offset += 8;
+
+ QVector2D newGradientEnd = QVector2D(newNode->fillGradient().b);
+ QVector2D oldGradientEnd = oldNode!= nullptr
+ ? QVector2D(oldNode->fillGradient().b)
+ : QVector2D{};
+
+ if (newGradientEnd != oldGradientEnd || oldEffect == nullptr) {
+ memcpy(buf->data() + offset, &newGradientEnd, 8);
+ changed = true;
+ }
+
+ offset += 8;
+ } else if (newNode->gradientType() == QQuickAbstractPathRenderer::RadialGradient) {
+ Q_ASSERT(buf->size() >= offset + 8 + 8 + 4 + 4);
+
+ QVector2D newFocalPoint = QVector2D(newNode->fillGradient().b);
+ QVector2D oldFocalPoint = oldNode != nullptr
+ ? QVector2D(oldNode->fillGradient().b)
+ : QVector2D{};
+ if (oldNode == nullptr || newFocalPoint != oldFocalPoint) {
+ memcpy(buf->data() + offset, &newFocalPoint, 8);
+ changed = true;
+ }
+ offset += 8;
+
+ QVector2D newCenterPoint = QVector2D(newNode->fillGradient().a);
+ QVector2D oldCenterPoint = oldNode != nullptr
+ ? QVector2D(oldNode->fillGradient().a)
+ : QVector2D{};
+
+ QVector2D newCenterToFocal = newCenterPoint - newFocalPoint;
+ QVector2D oldCenterToFocal = oldCenterPoint - oldFocalPoint;
+ if (oldNode == nullptr || newCenterToFocal != oldCenterToFocal) {
+ memcpy(buf->data() + offset, &newCenterToFocal, 8);
+ changed = true;
+ }
+ offset += 8;
+
+ float newCenterRadius = newNode->fillGradient().v0;
+ float oldCenterRadius = oldNode != nullptr
+ ? oldNode->fillGradient().v0
+ : 0.0f;
+ if (oldNode == nullptr || !qFuzzyCompare(newCenterRadius, oldCenterRadius)) {
+ memcpy(buf->data() + offset, &newCenterRadius, 4);
+ changed = true;
+ }
+ offset += 4;
+
+ float newFocalRadius = newNode->fillGradient().v1;
+ float oldFocalRadius = oldNode != nullptr
+ ? oldNode->fillGradient().v1
+ : 0.0f;
+ if (oldNode == nullptr || !qFuzzyCompare(newFocalRadius, oldFocalRadius)) {
+ memcpy(buf->data() + offset, &newFocalRadius, 4);
+ changed = true;
+ }
+ offset += 4;
+
+ } else if (newNode->gradientType() == QQuickAbstractPathRenderer::ConicalGradient) {
+ Q_ASSERT(buf->size() >= offset + 8 + 4);
+
+ QVector2D newFocalPoint = QVector2D(newNode->fillGradient().a);
+ QVector2D oldFocalPoint = oldNode != nullptr
+ ? QVector2D(oldNode->fillGradient().a)
+ : QVector2D{};
+ if (oldNode == nullptr || newFocalPoint != oldFocalPoint) {
+ memcpy(buf->data() + offset, &newFocalPoint, 8);
+ changed = true;
+ }
+ offset += 8;
+
+ float newAngle = newNode->fillGradient().v0;
+ float oldAngle = oldNode != nullptr
+ ? oldNode->fillGradient().v0
+ : 0.0f;
+ if (oldNode == nullptr || !qFuzzyCompare(newAngle, oldAngle)) {
+ newAngle = -qDegreesToRadians(newAngle);
+ memcpy(buf->data() + offset, &newAngle, 4);
+ changed = true;
+ }
+ offset += 4;
+ }
+
+ return changed;
+ }
+}
+
+QQuickShapeCurveMaterial::QQuickShapeCurveMaterial(QQuickShapeCurveNode *node)
+ : m_node(node)
+{
+ setFlag(Blending, true);
+ setFlag(RequiresDeterminant, true);
+}
+
+int QQuickShapeCurveMaterial::compare(const QSGMaterial *other) const
+{
+ if (other->type() != type())
+ return (type() - other->type());
+
+ const QQuickShapeCurveMaterial *otherMaterial =
+ static_cast<const QQuickShapeCurveMaterial *>(other);
+
+ QQuickShapeCurveNode *a = node();
+ QQuickShapeCurveNode *b = otherMaterial->node();
+ if (a == b)
+ return 0;
+
+ if (int d = a->strokeColor().rgba() - b->strokeColor().rgba())
+ return d;
+
+ if (a->gradientType() == QQuickAbstractPathRenderer::NoGradient) {
+ if (int d = a->color().red() - b->color().red())
+ return d;
+ if (int d = a->color().green() - b->color().green())
+ return d;
+ if (int d = a->color().blue() - b->color().blue())
+ return d;
+ if (int d = a->color().alpha() - b->color().alpha())
+ return d;
+ } else {
+ const QQuickAbstractPathRenderer::GradientDesc &ga = a->fillGradient();
+ const QQuickAbstractPathRenderer::GradientDesc &gb = b->fillGradient();
+
+ if (int d = ga.a.x() - gb.a.x())
+ return d;
+ if (int d = ga.a.y() - gb.a.y())
+ return d;
+ if (int d = ga.b.x() - gb.b.x())
+ return d;
+ if (int d = ga.b.y() - gb.b.y())
+ return d;
+
+ if (int d = ga.v0 - gb.v0)
+ return d;
+ if (int d = ga.v1 - gb.v1)
+ return d;
+
+ if (int d = ga.spread - gb.spread)
+ return d;
+
+ if (int d = ga.stops.size() - gb.stops.size())
+ return d;
+
+ for (int i = 0; i < ga.stops.size(); ++i) {
+ if (int d = ga.stops[i].first - gb.stops[i].first)
+ return d;
+ if (int d = ga.stops[i].second.rgba() - gb.stops[i].second.rgba())
+ return d;
+ }
+ }
+
+ return 0;
+}
+
+QSGMaterialType *QQuickShapeCurveMaterial::type() const
+{
+ static QSGMaterialType type[8];
+ uint index = node()->gradientType();
+ Q_ASSERT((index & ~3) == 0); // Only two first bits for gradient type
+
+ if (node()->hasStroke())
+ index |= 4;
+
+ return &type[index];
+}
+
+QSGMaterialShader *QQuickShapeCurveMaterial::createShader(QSGRendererInterface::RenderMode renderMode) const
+{
+ return new QQuickShapeCurveMaterialShader(node()->gradientType(),
+ node()->hasStroke(),
+ renderMode == QSGRendererInterface::RenderMode3D);
+}
+
+
+QT_END_NAMESPACE
diff --git a/src/quickshapes/qquickshapecurvenode_p.h b/src/quickshapes/qquickshapecurvenode_p.h
new file mode 100644
index 0000000000..0e66ac1271
--- /dev/null
+++ b/src/quickshapes/qquickshapecurvenode_p.h
@@ -0,0 +1,193 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKSHAPECURVENODE_P_H
+#define QQUICKSHAPECURVENODE_P_H
+
+#include <QtQuick/qsgnode.h>
+
+#include "qquickshapegenericrenderer_p.h"
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of a number of Qt sources files. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+QT_BEGIN_NAMESPACE
+
+class QQuickShapeCurveNode : public QSGGeometryNode
+{
+public:
+ QQuickShapeCurveNode();
+
+ void setColor(QColor col)
+ {
+ m_color = col;
+ }
+
+ QColor color() const
+ {
+ return m_color;
+ }
+
+ void setStrokeColor(QColor col)
+ {
+ const bool hadStroke = hasStroke();
+ m_strokeColor = col;
+ if (hadStroke != hasStroke())
+ updateMaterial();
+ }
+
+ QColor strokeColor() const
+ {
+ return m_strokeColor;
+ }
+
+ void setStrokeWidth(float width)
+ {
+ const bool hadStroke = hasStroke();
+ m_strokeWidth = width;
+ if (hadStroke != hasStroke())
+ updateMaterial();
+ }
+
+ float strokeWidth() const
+ {
+ return m_strokeWidth;
+ }
+
+ void setFillGradient(const QQuickAbstractPathRenderer::GradientDesc &fillGradient)
+ {
+ m_fillGradient = fillGradient;
+ }
+
+ QQuickAbstractPathRenderer::GradientDesc fillGradient() const
+ {
+ return m_fillGradient;
+ }
+
+ void setGradientType(QQuickAbstractPathRenderer::FillGradientType type)
+ {
+ if (m_gradientType != type) {
+ m_gradientType = type;
+ updateMaterial();
+ }
+ }
+
+ QQuickAbstractPathRenderer::FillGradientType gradientType() const
+ {
+ return m_gradientType;
+ }
+
+ bool hasStroke() const
+ {
+ return m_strokeWidth > 0.0f && m_strokeColor.alpha() > 0;
+ }
+
+ void appendTriangle(const QVector2D &v1,
+ const QVector2D &v2,
+ const QVector2D &v3,
+ std::function<QVector3D(QVector2D)> uvForPoint,
+ QVector4D debugColor1,
+ QVector4D debugColor2,
+ QVector4D debugColor3)
+ {
+ QVector3D uv1 = uvForPoint(v1);
+ QVector3D uv2 = uvForPoint(v2);
+ QVector3D uv3 = uvForPoint(v3);
+
+ QVector2D duvdx = QVector2D(uvForPoint(v1 + QVector2D(1, 0))) - QVector2D(uv1);
+ QVector2D duvdy = QVector2D(uvForPoint(v1 + QVector2D(0, 1))) - QVector2D(uv1);
+
+ m_uncookedIndexes.append(m_uncookedVertexes.size());
+ m_uncookedVertexes.append( { v1.x(), v1.y(),
+ uv1.x(), uv1.y(), uv1.z(),
+ debugColor1.x(), debugColor1.y(), debugColor1.z(), debugColor1.w(),
+ duvdx.x(), duvdx.y(),
+ duvdy.x(), duvdy.y()
+ });
+
+ m_uncookedIndexes.append(m_uncookedVertexes.size());
+ m_uncookedVertexes.append( { v2.x(), v2.y(),
+ uv2.x(), uv2.y(), uv2.z(),
+ debugColor2.x(), debugColor2.y(), debugColor2.z(), debugColor2.w(),
+ duvdx.x(), duvdx.y(),
+ duvdy.x(), duvdy.y()
+ });
+
+ m_uncookedIndexes.append(m_uncookedVertexes.size());
+ m_uncookedVertexes.append( { v3.x(), v3.y(),
+ uv3.x(), uv3.y(), uv3.z(),
+ debugColor3.x(), debugColor3.y(), debugColor3.z(), debugColor3.w(),
+ duvdx.x(), duvdx.y(),
+ duvdy.x(), duvdy.y()
+ });
+ }
+
+ void appendVertex(const QVector2D &vertex,
+ std::function<QVector3D(QVector2D)> uvForPoint,
+ const QVector4D &debugColor)
+ {
+ QVector3D uv = uvForPoint(vertex);
+
+ QVector2D duvdx = QVector2D(uvForPoint(vertex + QVector2D(1, 0))) - QVector2D(uv);
+ QVector2D duvdy = QVector2D(uvForPoint(vertex + QVector2D(0, 1))) - QVector2D(uv);
+
+ m_uncookedVertexes.append( { vertex.x(), vertex.y(),
+ uv.x(), uv.y(), uv.z(),
+ debugColor.x(), debugColor.y(), debugColor.z(), debugColor.w(),
+ duvdx.x(), duvdx.y(),
+ duvdy.x(), duvdy.y()
+ }
+ );
+ }
+
+ void appendIndex(quint32 index)
+ {
+ m_uncookedIndexes.append(index);
+ }
+
+ void appendIndexes(QVector<quint32> indexes)
+ {
+ m_uncookedIndexes.append(indexes);
+ }
+
+ QVector<quint32> uncookedIndexes() const
+ {
+ return m_uncookedIndexes;
+ }
+
+ void cookGeometry();
+
+private:
+ struct CurveNodeVertex
+ {
+ float x, y, u, v, w;
+ float r, g, b, a; // Debug color, mixed in proportion to a
+ float dudx, dvdx, dudy, dvdy; // Size of pixel in curve space (must be same for all vertices in triangle)
+ };
+
+ void updateMaterial();
+ static const QSGGeometry::AttributeSet &attributes();
+
+ QColor m_color = Qt::white;
+ QColor m_strokeColor = Qt::transparent;
+ float m_strokeWidth = 0.0f;
+ QQuickAbstractPathRenderer::GradientDesc m_fillGradient;
+ QQuickAbstractPathRenderer::FillGradientType m_gradientType = QQuickAbstractPathRenderer::NoGradient;
+
+ QScopedPointer<QSGMaterial> m_material;
+
+ QVector<CurveNodeVertex> m_uncookedVertexes;
+ QVector<quint32> m_uncookedIndexes;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKSHAPECURVENODE_P_H
diff --git a/src/quickshapes/qquickshapecurvenode_p_p.h b/src/quickshapes/qquickshapecurvenode_p_p.h
new file mode 100644
index 0000000000..5b04785e31
--- /dev/null
+++ b/src/quickshapes/qquickshapecurvenode_p_p.h
@@ -0,0 +1,43 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKSHAPECURVENODE_P_P_H
+#define QQUICKSHAPECURVENODE_P_P_H
+
+#include <QtQuick/qsgmaterial.h>
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of a number of Qt sources files. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+QT_BEGIN_NAMESPACE
+
+class QQuickShapeCurveNode;
+class QQuickShapeCurveMaterial : public QSGMaterial
+{
+public:
+ QQuickShapeCurveMaterial(QQuickShapeCurveNode *node);
+ int compare(const QSGMaterial *other) const override;
+
+ QQuickShapeCurveNode *node() const
+ {
+ return m_node;
+ }
+
+private:
+ QSGMaterialType *type() const override;
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const override;
+
+ QQuickShapeCurveNode *m_node;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKSHAPECURVENODE_P_P_H
diff --git a/src/quickshapes/qquickshapecurverenderer.cpp b/src/quickshapes/qquickshapecurverenderer.cpp
index 2ab282c6af..a20db675e2 100644
--- a/src/quickshapes/qquickshapecurverenderer.cpp
+++ b/src/quickshapes/qquickshapecurverenderer.cpp
@@ -3,9 +3,11 @@
#include "qquickshapecurverenderer_p.h"
#include "qquickshapecurverenderer_p_p.h"
-#include "qquickshapegenericrenderer_p.h"
+#include "qquickshapecurvenode_p.h"
+#include "qquickshapestrokenode_p.h"
#include <QtGui/qvector2d.h>
+#include <QtGui/qvector4d.h>
#include <QtGui/private/qtriangulator_p.h>
#include <QtGui/private/qtriangulatingstroker_p.h>
#include <QtGui/private/qrhi_p.h>
@@ -22,208 +24,22 @@ Q_LOGGING_CATEGORY(lcShapeCurveRenderer, "qt.shape.curverenderer");
# define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN (1.0f / 32.0f)
#endif
-#define QQUICKSHAPECURVERENDERER_GRADIENTS
-
namespace {
- class QQuickShapeWireFrameMaterialShader : public QSGMaterialShader
- {
- public:
- QQuickShapeWireFrameMaterialShader()
- {
- setShaderFileName(VertexStage,
- QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.vert.qsb"));
- setShaderFileName(FragmentStage,
- QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.frag.qsb"));
- }
-
- bool updateUniformData(RenderState &state, QSGMaterial *, QSGMaterial *) override
- {
- bool changed = false;
- QByteArray *buf = state.uniformData();
- Q_ASSERT(buf->size() >= 64);
-
- if (state.isMatrixDirty()) {
- const QMatrix4x4 m = state.combinedMatrix();
-
- memcpy(buf->data(), m.constData(), 64);
- changed = true;
- }
-
- return changed;
- }
- };
-
- class QQuickShapeWireFrameMaterial : public QSGMaterial
- {
- public:
- QQuickShapeWireFrameMaterial()
- {
- setFlag(Blending, true);
- }
-
- int compare(const QSGMaterial *other) const override
- {
- return (type() - other->type());
- }
-
- protected:
- QSGMaterialType *type() const override
- {
- static QSGMaterialType t;
- return &t;
- }
- QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override
- {
- return new QQuickShapeWireFrameMaterialShader;
- }
-
- };
-
- class QQuickShapeWireFrameNode : public QSGGeometryNode
- {
- public:
- struct WireFrameVertex
- {
- float x, y, u, v, w;
- };
-
- QQuickShapeWireFrameNode()
- {
- setFlag(OwnsGeometry, true);
- setGeometry(new QSGGeometry(attributes(), 0, 0));
- activateMaterial();
- }
-
- void activateMaterial()
- {
- m_material.reset(new QQuickShapeWireFrameMaterial);
- setMaterial(m_material.data());
- }
-
- static const QSGGeometry::AttributeSet &attributes()
- {
- static QSGGeometry::Attribute data[] = {
- QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute),
- QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
- };
- static QSGGeometry::AttributeSet attrs = { 2, sizeof(WireFrameVertex), data };
- return attrs;
- }
-
- protected:
- QScopedPointer<QQuickShapeWireFrameMaterial> m_material;
- };
- class QQuickShapeLoopBlinnNode;
- class QQuickShapeLoopBlinnMaterial : public QSGMaterial
- {
- public:
- QQuickShapeLoopBlinnMaterial(QQuickShapeLoopBlinnNode *node,
- QQuickAbstractPathRenderer::FillGradientType gradientType)
- : m_node(node)
- , m_gradientType(gradientType)
- {
- setFlag(Blending, true);
- }
- int compare(const QSGMaterial *other) const override;
-
- QQuickShapeLoopBlinnNode *node() const
- {
- return m_node;
- }
-
- protected:
- QSGMaterialType *type() const override;
- QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const override;
-
- QQuickShapeLoopBlinnNode *m_node;
- QQuickAbstractPathRenderer::FillGradientType m_gradientType;
- };
-
- class QQuickShapeLoopBlinnNode : public QSGGeometryNode
- {
- public:
- QQuickShapeLoopBlinnNode(QQuickAbstractPathRenderer::FillGradientType gradientType);
- struct LoopBlinnVertex
- {
- float x, y, u, v, w;
- float r, g, b, a; // Debug color, mixed in proportion to a
- };
- static const QSGGeometry::AttributeSet &attributes()
- {
- static QSGGeometry::Attribute data[] = {
- QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute),
- QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
- QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::ColorAttribute),
- };
- static QSGGeometry::AttributeSet attrs = { 3, sizeof(LoopBlinnVertex), data };
- return attrs;
- }
-
- QColor color;
- QColor strokeColor = Qt::transparent;
- float strokeWidth = 0.0f;
- QQuickAbstractPathRenderer::GradientDesc fillGradient;
-
- protected:
- void activateMaterial(QQuickAbstractPathRenderer::FillGradientType gradientType);
-
- QScopedPointer<QSGMaterial> m_material;
- };
-
- class QQuickShapeLoopBlinnMaterialShader : public QSGMaterialShader
- {
- public:
- QQuickShapeLoopBlinnMaterialShader(QQuickAbstractPathRenderer::FillGradientType gradientType,
- bool includeStroke);
-
- bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override;
- void updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
- QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
-
- private:
- QQuickAbstractPathRenderer::FillGradientType m_gradientType;
- };
-
- QQuickShapeLoopBlinnMaterialShader::QQuickShapeLoopBlinnMaterialShader(QQuickAbstractPathRenderer::FillGradientType gradientType,
- bool includeStroke)
- : m_gradientType(gradientType)
- {
- QString baseName = QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapecurve");
-
- if (gradientType == QQuickAbstractPathRenderer::LinearGradient) {
- baseName += QStringLiteral("_lg");
- } else if (gradientType == QQuickAbstractPathRenderer::RadialGradient) {
- baseName += QStringLiteral("_rg");
- } else if (gradientType == QQuickAbstractPathRenderer::ConicalGradient) {
- baseName += QStringLiteral("_cg");
- }
-
- if (includeStroke)
- baseName += QStringLiteral("_stroke");
-
- setShaderFileName(VertexStage, baseName + QStringLiteral(".vert.qsb"));
- setShaderFileName(FragmentStage, baseName + QStringLiteral(".frag.qsb"));
- }
-
- void QQuickShapeLoopBlinnMaterialShader::updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
- QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+class QQuickShapeWireFrameMaterialShader : public QSGMaterialShader
+{
+public:
+ QQuickShapeWireFrameMaterialShader()
{
- Q_UNUSED(oldMaterial);
- if (binding != 1 || m_gradientType == QQuickAbstractPathRenderer::NoGradient)
- return;
-
- QQuickShapeLoopBlinnMaterial *m = static_cast<QQuickShapeLoopBlinnMaterial *>(newMaterial);
- QQuickShapeLoopBlinnNode *node = m->node();
- const QQuickShapeGradientCacheKey cacheKey(node->fillGradient.stops, node->fillGradient.spread);
- QSGTexture *t = QQuickShapeGradientCache::cacheForRhi(state.rhi())->get(cacheKey);
- t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());
- *texture = t;
+ setShaderFileName(VertexStage,
+ QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.vert.qsb"));
+ setShaderFileName(FragmentStage,
+ QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.frag.qsb"));
}
- bool QQuickShapeLoopBlinnMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect)
+ bool updateUniformData(RenderState &state, QSGMaterial *, QSGMaterial *) override
{
bool changed = false;
QByteArray *buf = state.uniformData();
@@ -235,249 +51,71 @@ namespace {
memcpy(buf->data(), m.constData(), 64);
changed = true;
}
- int offset = 64;
-
- QQuickShapeLoopBlinnMaterial *newMaterial = static_cast<QQuickShapeLoopBlinnMaterial *>(newEffect);
- QQuickShapeLoopBlinnMaterial *oldMaterial = static_cast<QQuickShapeLoopBlinnMaterial *>(oldEffect);
-
- QQuickShapeLoopBlinnNode *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr;
- QQuickShapeLoopBlinnNode *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr;
-
- if (newNode == nullptr)
- return changed;
-
- if (newNode->strokeColor.alpha() > 0 && newNode->strokeWidth > 0.0f) {
- QVector4D newStrokeColor(newNode->strokeColor.redF(),
- newNode->strokeColor.greenF(),
- newNode->strokeColor.blueF(),
- newNode->strokeColor.alphaF());
- QVector4D oldStrokeColor = oldNode != nullptr
- ? QVector4D(oldNode->strokeColor.redF(),
- oldNode->strokeColor.greenF(),
- oldNode->strokeColor.blueF(),
- oldNode->strokeColor.alphaF())
- : QVector4D{};
-
- if (oldNode == nullptr || oldStrokeColor != newStrokeColor) {
- memcpy(buf->data() + offset, &newStrokeColor, 16);
- changed = true;
- }
- offset += 16;
-
- if (oldNode == nullptr || !qFuzzyCompare(newNode->strokeWidth, oldNode->strokeWidth || (state.isMatrixDirty() && newNode->strokeWidth > 0 ))) {
- float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio();
- float w = newNode->strokeWidth * matrixScale;
- memcpy(buf->data() + offset, &w, 4);
- changed = true;
- }
- offset += 16;
- }
-
- if (m_gradientType == QQuickAbstractPathRenderer::NoGradient) {
- Q_ASSERT(buf->size() >= offset + 16);
-
- QVector4D newColor = QVector4D(newNode->color.redF(),
- newNode->color.greenF(),
- newNode->color.blueF(),
- newNode->color.alphaF());
- QVector4D oldColor = oldNode != nullptr
- ? QVector4D(oldNode->color.redF(),
- oldNode->color.greenF(),
- oldNode->color.blueF(),
- oldNode->color.alphaF())
- : QVector4D{};
-
- if (oldColor != newColor) {
- memcpy(buf->data() + offset, &newColor, 16);
- changed = true;
- }
-
- offset += 16;
- } else if (m_gradientType == QQuickAbstractPathRenderer::LinearGradient) {
- Q_ASSERT(buf->size() >= offset + 8 + 8);
-
- QVector2D newGradientStart = QVector2D(newNode->fillGradient.a);
- QVector2D oldGradientStart = oldNode != nullptr
- ? QVector2D(oldNode->fillGradient.a)
- : QVector2D{};
-
- if (newGradientStart != oldGradientStart || oldEffect == nullptr) {
- memcpy(buf->data() + offset, &newGradientStart, 8);
- changed = true;
- }
- offset += 8;
-
- QVector2D newGradientEnd = QVector2D(newNode->fillGradient.b);
- QVector2D oldGradientEnd = oldNode!= nullptr
- ? QVector2D(oldNode->fillGradient.b)
- : QVector2D{};
-
- if (newGradientEnd != oldGradientEnd || oldEffect == nullptr) {
- memcpy(buf->data() + offset, &newGradientEnd, 8);
- changed = true;
- }
-
- offset += 8;
- } else if (newNode != nullptr && m_gradientType == QQuickAbstractPathRenderer::RadialGradient) {
- Q_ASSERT(buf->size() >= offset + 8 + 8 + 4 + 4);
-
- QVector2D newFocalPoint = QVector2D(newNode->fillGradient.b);
- QVector2D oldFocalPoint = oldNode != nullptr
- ? QVector2D(oldNode->fillGradient.b)
- : QVector2D{};
- if (oldNode == nullptr || newFocalPoint != oldFocalPoint) {
- memcpy(buf->data() + offset, &newFocalPoint, 8);
- changed = true;
- }
- offset += 8;
-
- QVector2D newCenterPoint = QVector2D(newNode->fillGradient.a);
- QVector2D oldCenterPoint = oldNode != nullptr
- ? QVector2D(oldNode->fillGradient.a)
- : QVector2D{};
-
- QVector2D newCenterToFocal = newCenterPoint - newFocalPoint;
- QVector2D oldCenterToFocal = oldCenterPoint - oldFocalPoint;
- if (oldNode == nullptr || newCenterToFocal != oldCenterToFocal) {
- memcpy(buf->data() + offset, &newCenterToFocal, 8);
- changed = true;
- }
- offset += 8;
-
- float newCenterRadius = newNode->fillGradient.v0;
- float oldCenterRadius = oldNode != nullptr
- ? oldNode->fillGradient.v0
- : 0.0f;
- if (oldNode == nullptr || !qFuzzyCompare(newCenterRadius, oldCenterRadius)) {
- memcpy(buf->data() + offset, &newCenterRadius, 4);
- changed = true;
- }
- offset += 4;
-
- float newFocalRadius = newNode->fillGradient.v1;
- float oldFocalRadius = oldNode != nullptr
- ? oldNode->fillGradient.v1
- : 0.0f;
- if (oldNode == nullptr || !qFuzzyCompare(newFocalRadius, oldFocalRadius)) {
- memcpy(buf->data() + offset, &newFocalRadius, 4);
- changed = true;
- }
- offset += 4;
-
- } else if (m_gradientType == QQuickAbstractPathRenderer::ConicalGradient) {
- Q_ASSERT(buf->size() >= offset + 8 + 4);
-
- QVector2D newFocalPoint = QVector2D(newNode->fillGradient.a);
- QVector2D oldFocalPoint = oldNode != nullptr
- ? QVector2D(oldNode->fillGradient.a)
- : QVector2D{};
- if (oldNode == nullptr || newFocalPoint != oldFocalPoint) {
- memcpy(buf->data() + offset, &newFocalPoint, 8);
- changed = true;
- }
- offset += 8;
-
- float newAngle = newNode->fillGradient.v0;
- float oldAngle = oldNode != nullptr
- ? oldNode->fillGradient.v0
- : 0.0f;
- if (oldNode == nullptr || !qFuzzyCompare(newAngle, oldAngle)) {
- newAngle = -qDegreesToRadians(newAngle);
- memcpy(buf->data() + offset, &newAngle, 4);
- changed = true;
- }
- offset += 4;
- }
return changed;
}
+};
- int QQuickShapeLoopBlinnMaterial::compare(const QSGMaterial *other) const
+class QQuickShapeWireFrameMaterial : public QSGMaterial
+{
+public:
+ QQuickShapeWireFrameMaterial()
{
- if (other->type() != type())
- return (type() - other->type());
-
- const QQuickShapeLoopBlinnMaterial *otherMaterial =
- static_cast<const QQuickShapeLoopBlinnMaterial *>(other);
-
- QQuickShapeLoopBlinnNode *a = node();
- QQuickShapeLoopBlinnNode *b = otherMaterial->node();
- if (a == b)
- return 0;
-
- if (int d = a->strokeColor.rgba() - b->strokeColor.rgba())
- return d;
-
- if (m_gradientType == QQuickAbstractPathRenderer::NoGradient) {
- if (int d = a->color.red() - b->color.red())
- return d;
- if (int d = a->color.green() - b->color.green())
- return d;
- if (int d = a->color.blue() - b->color.blue())
- return d;
- if (int d = a->color.alpha() - b->color.alpha())
- return d;
- } else {
- const QQuickAbstractPathRenderer::GradientDesc &ga = a->fillGradient;
- const QQuickAbstractPathRenderer::GradientDesc &gb = b->fillGradient;
-
- if (int d = ga.a.x() - gb.a.x())
- return d;
- if (int d = ga.a.y() - gb.a.y())
- return d;
- if (int d = ga.b.x() - gb.b.x())
- return d;
- if (int d = ga.b.y() - gb.b.y())
- return d;
-
- if (int d = ga.v0 - gb.v0)
- return d;
- if (int d = ga.v1 - gb.v1)
- return d;
-
- if (int d = ga.spread - gb.spread)
- return d;
-
- if (int d = ga.stops.size() - gb.stops.size())
- return d;
-
- for (int i = 0; i < ga.stops.size(); ++i) {
- if (int d = ga.stops[i].first - gb.stops[i].first)
- return d;
- if (int d = ga.stops[i].second.rgba() - gb.stops[i].second.rgba())
- return d;
- }
- }
-
- return 0;
+ setFlag(Blending, true);
}
- QSGMaterialType *QQuickShapeLoopBlinnMaterial::type() const
+ int compare(const QSGMaterial *other) const override
{
- static QSGMaterialType type[8];
- return &type[m_gradientType + (node()->strokeColor.alpha() > 0 ? 4 : 0)];
+ return (type() - other->type());
}
- QSGMaterialShader *QQuickShapeLoopBlinnMaterial::createShader(QSGRendererInterface::RenderMode renderMode) const
+protected:
+ QSGMaterialType *type() const override
{
- Q_UNUSED(renderMode);
- return new QQuickShapeLoopBlinnMaterialShader(m_gradientType,
- node()->strokeColor.alpha() > 0);
+ static QSGMaterialType t;
+ return &t;
}
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override
+ {
+ return new QQuickShapeWireFrameMaterialShader;
+ }
+
+};
+
+class QQuickShapeWireFrameNode : public QSGGeometryNode
+{
+public:
+ struct WireFrameVertex
+ {
+ float x, y, u, v, w;
+ };
- QQuickShapeLoopBlinnNode::QQuickShapeLoopBlinnNode(QQuickAbstractPathRenderer::FillGradientType gradientType)
+ QQuickShapeWireFrameNode()
{
setFlag(OwnsGeometry, true);
setGeometry(new QSGGeometry(attributes(), 0, 0));
-
- activateMaterial(gradientType);
+ activateMaterial();
}
- void QQuickShapeLoopBlinnNode::activateMaterial(QQuickAbstractPathRenderer::FillGradientType gradientType)
+ void activateMaterial()
{
- m_material.reset(new QQuickShapeLoopBlinnMaterial(this, gradientType));
+ m_material.reset(new QQuickShapeWireFrameMaterial);
setMaterial(m_material.data());
}
+
+ static const QSGGeometry::AttributeSet &attributes()
+ {
+ static QSGGeometry::Attribute data[] = {
+ QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute),
+ QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
+ };
+ static QSGGeometry::AttributeSet attrs = { 2, sizeof(WireFrameVertex), data };
+ return attrs;
+ }
+
+protected:
+ QScopedPointer<QQuickShapeWireFrameMaterial> m_material;
+};
}
QVector2D QuadPath::Element::pointAtFraction(float t) const
@@ -550,10 +188,17 @@ bool QuadPath::isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVec
return qFuzzyIsNull(crossProduct(p, sp, ep));
}
+// Assumes sp != ep
bool QuadPath::isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
{
- constexpr float epsilon = 1.0f;
- return qAbs(crossProduct(p, sp, ep)) < epsilon;
+ // epsilon is max length of p-to-baseline relative to length of baseline. So 0.01 means that
+ // the distance from p to the baseline must be less than 1% of the length of the baseline.
+ constexpr float epsilon = 0.01f;
+ QVector2D bv = ep - sp;
+ float bl2 = QVector2D::dotProduct(bv, bv);
+ float t = QVector2D::dotProduct(p - sp, bv) / bl2;
+ QVector2D pv = p - (sp + t * bv);
+ return (QVector2D::dotProduct(pv, pv) / bl2) < (epsilon * epsilon);
}
bool QuadPath::isControlPointOnLeft(const QuadPath::Element &element)
@@ -717,6 +362,7 @@ void QuadPath::addCurvatureData()
qDebug() << "Curvature anomaly detected:" << element
<< "Subpath fill on right:" << (flags & Element::FillOnRight)
<< "Element fill on right:" << (newFlags & Element::FillOnRight);
+ flags = newFlags;
}
}
@@ -846,6 +492,173 @@ QuadPath QuadPath::flattened() const
return res;
}
+class ElementCutter
+{
+public:
+ ElementCutter(const QuadPath::Element &element)
+ : m_element(element)
+ {
+ m_currentPoint = m_element.startPoint();
+ if (m_element.isLine())
+ m_lineLength = (m_element.endPoint() - m_element.startPoint()).length();
+ else
+ fillLUT();
+ }
+
+ bool consume(float length)
+ {
+ m_lastT = m_currentT;
+ m_lastPoint = m_currentPoint;
+ float nextBreak = m_consumed + length;
+ float breakT = m_element.isLine() ? nextBreak / m_lineLength : tForLength(nextBreak);
+ if (breakT < 1) {
+ m_currentT = breakT;
+ m_currentPoint = m_element.pointAtFraction(m_currentT);
+ m_consumed = nextBreak;
+ return true;
+ } else {
+ m_currentT = 1;
+ m_currentPoint = m_element.endPoint();
+ return false;
+ }
+ }
+
+ QVector2D currentCutPoint()
+ {
+ return m_currentPoint;
+ }
+
+ QVector2D currentControlPoint()
+ {
+ Q_ASSERT(!m_element.isLine());
+ // Split curve right at lastT, yields { lastPoint, rcp, endPoint } quad segment
+ QVector2D rcp = (1 - m_lastT) * m_element.controlPoint() + m_lastT * m_element.endPoint();
+ // Split that left at currentT, yields { lastPoint, lcp, currentPoint } quad segment
+ float segmentT = (m_currentT - m_lastT) / (1 - m_lastT);
+ QVector2D lcp = (1 - segmentT) * m_lastPoint + segmentT * rcp;
+ return lcp;
+ }
+
+ float lastLength()
+ {
+ float elemLength = m_element.isLine() ? m_lineLength : m_lut.last();
+ return elemLength - m_consumed;
+ }
+
+private:
+ void fillLUT()
+ {
+ Q_ASSERT(!m_element.isLine());
+ QVector2D ap = m_element.startPoint() - 2 * m_element.controlPoint() + m_element.endPoint();
+ QVector2D bp = 2 * m_element.controlPoint() - 2 * m_element.startPoint();
+ float A = 4 * QVector2D::dotProduct(ap, ap);
+ float B = 4 * QVector2D::dotProduct(ap, bp);
+ float C = QVector2D::dotProduct(bp, bp);
+ float b = B / (2 * A);
+ float c = C / A;
+ float k = c - (b * b);
+ float l2 = b * std::sqrt(b * b + k);
+ float lnom = b + std::sqrt(b * b + k);
+ float l0 = 0.5f * std::sqrt(A);
+
+ m_lut.resize(LUTSize, 0);
+ for (int i = 1; i < LUTSize; i++) {
+ float t = float(i) / (LUTSize - 1);
+ float u = t + b;
+ float w = std::sqrt(u * u + k);
+ float l1 = u * w;
+ float lden = u + w;
+ float l3 = k * std::log(std::fabs(lden / lnom));
+ float res = l0 * (l1 - l2 + l3);
+ m_lut[i] = res;
+ }
+ }
+
+ float tForLength(float length)
+ {
+ Q_ASSERT(!m_element.isLine());
+ Q_ASSERT(!m_lut.isEmpty());
+
+ float res = 2; // I.e. invalid, outside [0, 1] range
+ auto it = std::upper_bound(m_lut.cbegin(), m_lut.cend(), length);
+ if (it != m_lut.cend()) {
+ float nextLength = *it--;
+ float prevLength = *it;
+ int prevIndex = std::distance(m_lut.cbegin(), it);
+ float fraction = (length - prevLength) / (nextLength - prevLength);
+ res = (prevIndex + fraction) / (LUTSize - 1);
+ }
+ return res;
+ }
+
+ const QuadPath::Element &m_element;
+ float m_lastT = 0;
+ float m_currentT = 0;
+ QVector2D m_lastPoint;
+ QVector2D m_currentPoint;
+ float m_consumed = 0;
+ // For line elements:
+ float m_lineLength;
+ // For quadratic curve elements:
+ static constexpr int LUTSize = 21;
+ QVarLengthArray<float, LUTSize> m_lut;
+};
+
+QuadPath QuadPath::dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset) const
+{
+ QVarLengthArray<float, 16> pattern;
+ float patternLength = 0;
+ for (int i = 0; i < 2 * (dashPattern.length() / 2); i++) {
+ float dashLength = qMax(lineWidth * dashPattern[i], qreal(0));
+ pattern.append(dashLength);
+ patternLength += dashLength;
+ }
+ if (patternLength == 0)
+ return {};
+
+ int startIndex = 0;
+ float startOffset = std::fmod(lineWidth * dashOffset, patternLength);
+ if (startOffset < 0)
+ startOffset += patternLength;
+ for (float dashLength : pattern) {
+ if (dashLength > startOffset)
+ break;
+ startIndex++;
+ startOffset -= dashLength;
+ }
+
+ int dashIndex = startIndex;
+ float offset = startOffset;
+ QuadPath res;
+ for (int i = 0; i < elementCount(); i++) {
+ const Element &element = elementAt(i);
+ if (element.isSubpathStart()) {
+ res.moveTo(element.startPoint());
+ dashIndex = startIndex;
+ offset = startOffset;
+ }
+ ElementCutter cutter(element);
+ while (true) {
+ bool gotAll = cutter.consume(pattern.at(dashIndex) - offset);
+ QVector2D nextPoint = cutter.currentCutPoint();
+ if (dashIndex & 1)
+ res.moveTo(nextPoint); // gap
+ else if (element.isLine())
+ res.lineTo(nextPoint); // dash in line
+ else
+ res.quadTo(cutter.currentControlPoint(), nextPoint); // dash in curve
+ if (gotAll) {
+ offset = 0;
+ dashIndex = (dashIndex + 1) % pattern.size();
+ } else {
+ offset += cutter.lastLength();
+ break;
+ }
+ }
+ }
+ return res;
+}
+
void QuadPath::splitElementAt(qsizetype index)
{
const qsizetype newChildIndex = m_childElements.size();
@@ -917,8 +730,6 @@ QDebug operator<<(QDebug stream, const QuadPath &path)
return stream;
}
-QQuickShapeCurveNode::QQuickShapeCurveNode() { }
-
QQuickShapeCurveRenderer::~QQuickShapeCurveRenderer() { }
void QQuickShapeCurveRenderer::beginSync(int totalCount, bool *countChanged)
@@ -932,8 +743,7 @@ void QQuickShapeCurveRenderer::setPath(int index, const QQuickPath *path)
{
auto &pathData = m_paths[index];
pathData.originalPath = path->path();
- pathData.m_dirty |= GeometryDirty;
-
+ pathData.m_dirty |= PathDirty;
}
void QQuickShapeCurveRenderer::setStrokeColor(int index, const QColor &color)
@@ -942,7 +752,7 @@ void QQuickShapeCurveRenderer::setStrokeColor(int index, const QColor &color)
const bool wasVisible = pathData.isStrokeVisible();
pathData.pen.setColor(color);
if (pathData.isStrokeVisible() != wasVisible)
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= StrokeDirty;
else
pathData.m_dirty |= UniformsDirty;
}
@@ -956,7 +766,7 @@ void QQuickShapeCurveRenderer::setStrokeWidth(int index, qreal w)
} else {
pathData.validPenWidth = false;
}
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= StrokeDirty;
}
void QQuickShapeCurveRenderer::setFillColor(int index, const QColor &color)
@@ -965,7 +775,7 @@ void QQuickShapeCurveRenderer::setFillColor(int index, const QColor &color)
const bool wasVisible = pathData.isFillVisible();
pathData.fillColor = color;
if (pathData.isFillVisible() != wasVisible)
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= FillDirty;
else
pathData.m_dirty |= UniformsDirty;
}
@@ -974,7 +784,7 @@ void QQuickShapeCurveRenderer::setFillRule(int index, QQuickShapePath::FillRule
{
auto &pathData = m_paths[index];
pathData.fillRule = Qt::FillRule(fillRule);
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= PathDirty;
}
void QQuickShapeCurveRenderer::setJoinStyle(int index,
@@ -984,14 +794,14 @@ void QQuickShapeCurveRenderer::setJoinStyle(int index,
auto &pathData = m_paths[index];
pathData.pen.setJoinStyle(Qt::PenJoinStyle(joinStyle));
pathData.pen.setMiterLimit(miterLimit);
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= StrokeDirty;
}
void QQuickShapeCurveRenderer::setCapStyle(int index, QQuickShapePath::CapStyle capStyle)
{
auto &pathData = m_paths[index];
pathData.pen.setCapStyle(Qt::PenCapStyle(capStyle));
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= StrokeDirty;
}
void QQuickShapeCurveRenderer::setStrokeStyle(int index,
@@ -1005,12 +815,11 @@ void QQuickShapeCurveRenderer::setStrokeStyle(int index,
pathData.pen.setDashPattern(dashPattern);
pathData.pen.setDashOffset(dashOffset);
}
- pathData.m_dirty |= GeometryDirty;
+ pathData.m_dirty |= StrokeDirty;
}
void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *gradient)
{
-#if defined(QQUICKSHAPECURVERENDERER_GRADIENTS)
PathData &pd(m_paths[index]);
pd.gradientType = NoGradient;
if (QQuickShapeLinearGradient *g = qobject_cast<QQuickShapeLinearGradient *>(gradient)) {
@@ -1030,7 +839,6 @@ void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *g
pd.gradient.a = QPointF(g->centerX(), g->centerY());
pd.gradient.v0 = g->angle();
} else
-#endif
if (gradient != nullptr) {
static bool warned = false;
if (!warned) {
@@ -1044,7 +852,7 @@ void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *g
pd.gradient.spread = gradient->spread();
}
- pd.m_dirty |= GeometryDirty;
+ pd.m_dirty |= FillDirty;
}
void QQuickShapeCurveRenderer::setAsyncCallback(void (*callback)(void *), void *data)
@@ -1059,312 +867,72 @@ void QQuickShapeCurveRenderer::endSync(bool async)
Q_UNUSED(async);
}
-bool QQuickShapeCurveRenderer::PathData::useFragmentShaderStroker() const
-{
- static bool useStrokeShader = qEnvironmentVariableIntValue("QT_QUICKSHAPES_STROKER");
- return useStrokeShader && pen.style() == Qt::SolidLine && isFillVisible();
-}
-
void QQuickShapeCurveRenderer::updateNode()
{
if (!m_rootNode)
return;
- static const int codePath = qEnvironmentVariableIntValue("QT_QUICKSHAPES_ALTERNATIVE_CODE_PATH");
- static const int overlapSolving = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_OVERLAP_SOLVER");
- static bool newStrokeShader = qEnvironmentVariableIntValue("QT_QUICKSHAPES_EXPERIMENTAL_STROKER");
-
- auto addNodes = [&](const PathData &pathData, NodeList *debugNodes) {
- if (codePath == 1)
- return addPathNodesBasic(pathData, debugNodes);
- else
- return addPathNodesLineShader(pathData, debugNodes);
- };
+ static const bool doOverlapSolving = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_OVERLAP_SOLVER");
+ static const bool useTriangulatingStroker = qEnvironmentVariableIntValue("QT_QUICKSHAPES_TRIANGULATING_STROKER");
+ static const bool simplifyPath = qEnvironmentVariableIntValue("QT_QUICKSHAPES_SIMPLIFY_PATHS");
for (PathData &pathData : m_paths) {
- if (pathData.m_dirty & GeometryDirty) {
- deleteAndClear(&pathData.strokeNodes);
- deleteAndClear(&pathData.fillNodes);
- deleteAndClear(&pathData.debugNodes);
+ int dirtyFlags = pathData.m_dirty;
- static bool useTriangulatingStroker = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_PAINTERPATH_STROKER");
- bool createStrokePath = pathData.isStrokeVisible() && !pathData.useFragmentShaderStroker();
-
- static bool simplifyPath = qEnvironmentVariableIntValue("QT_QUICKSHAPES_SIMPLIFY_PATHS") != 0;
- pathData.path = simplifyPath ? QuadPath::fromPainterPath(pathData.originalPath.simplified()) : QuadPath::fromPainterPath(pathData.originalPath);
+ if (dirtyFlags & PathDirty) {
+ if (simplifyPath)
+ pathData.path = QuadPath::fromPainterPath(pathData.originalPath.simplified());
+ else
+ pathData.path = QuadPath::fromPainterPath(pathData.originalPath);
pathData.path.setFillRule(pathData.fillRule);
+ pathData.fillPath = {};
+ dirtyFlags |= (FillDirty | StrokeDirty);
+ }
- bool solidStroke = pathData.pen.style() == Qt::SolidLine && pathData.pen.isSolid();
- if (newStrokeShader && solidStroke) {
- pathData.qPath = pathData.path;
- pathData.qPath.addCurvatureData(); // ### Can we unify with fill below?
- } else if (createStrokePath) {
- pathData.fillPath = pathData.path.toPainterPath(); // Without subpath closing etc.
- }
-
+ if (dirtyFlags & FillDirty) {
+ deleteAndClear(&pathData.fillNodes);
+ deleteAndClear(&pathData.fillDebugNodes);
if (pathData.isFillVisible()) {
- pathData.path = pathData.path.subPathsClosed();
- pathData.path.addCurvatureData();
- if (overlapSolving)
- solveOverlaps(pathData.path);
- pathData.fillNodes = addNodes(pathData, &pathData.debugNodes);
- }
-
- if (createStrokePath) {
- if (newStrokeShader && solidStroke) {
- pathData.strokeNodes = addNodesStrokeShader(pathData, &pathData.debugNodes);
- } else if (useTriangulatingStroker && solidStroke) {
- pathData.strokeNodes = addStrokeNodes(pathData, &pathData.debugNodes);
- } else {
- QPainterPathStroker stroker(pathData.pen);
- QPainterPath strokePath = stroker.createStroke(pathData.fillPath);
-
- // Solid strokes are sometimes created with self-overlaps in the joins,
- // causing the overlap detection to freak out. So while this is expensive
- // we have to make sure the overlaps are removed first.
- static bool simplifyStroke = qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_SIMPLIFY_STROKE") == 0;
- if (pathData.pen.style() == Qt::SolidLine && simplifyStroke)
- strokePath = strokePath.simplified();
- QuadPath strokeQuadPath = QuadPath::fromPainterPath(strokePath);
- strokeQuadPath.addCurvatureData();
- strokePath = strokeQuadPath.toPainterPath();
- if (overlapSolving)
- solveOverlaps(strokeQuadPath);
-
- PathData strokeData = pathData;
- strokeData.path = strokeQuadPath;
- strokeData.fillPath = strokePath;
- strokeData.gradientType = NoGradient;
- strokeData.fillColor = pathData.pen.color();
- pathData.strokeNodes = addNodes(strokeData, &pathData.debugNodes);
+ if (pathData.fillPath.isEmpty()) {
+ pathData.fillPath = pathData.path.subPathsClosed();
+ pathData.fillPath.addCurvatureData();
+ if (doOverlapSolving)
+ solveOverlaps(pathData.fillPath);
}
+ pathData.fillNodes = addFillNodes(pathData, &pathData.fillDebugNodes);
+ dirtyFlags |= StrokeDirty;
}
- } else if (pathData.m_dirty & UniformsDirty) {
- for (auto &pathNode : std::as_const(pathData.fillNodes))
- static_cast<QQuickShapeLoopBlinnNode *>(pathNode)->color = pathData.fillColor;
-
- if (pathData.pen.style() != Qt::NoPen && pathData.pen.color().alpha() > 0) {
- for (auto &strokeNode : std::as_const(pathData.strokeNodes))
- if (newStrokeShader) // #### Remove this when we have a dedicated stroke shader
- static_cast<QQuickShapeLoopBlinnNode *>(strokeNode)->strokeColor = pathData.pen.color();
- else
- static_cast<QQuickShapeLoopBlinnNode *>(strokeNode)->color = pathData.pen.color();
- }
- }
- pathData.m_dirty &= ~(GeometryDirty | UniformsDirty);
- }
-}
-
-QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesBasic(const PathData &pathData, NodeList *debugNodes)
-{
- QVector<QSGGeometryNode *> ret;
-
- QList<QPolygonF> quadraticCurvesConvex;
- QList<QPolygonF> quadraticCurvesConcave;
- QList<QLineF> lineSegments;
- QPainterPath internalHull;
- internalHull.setFillRule(pathData.path.fillRule());
-
- pathData.path.iterateElements([&](const QuadPath::Element &element){
- QPointF sp(element.startPoint().toPointF());
- QPointF cp(element.controlPoint().toPointF());
- QPointF ep(element.endPoint().toPointF());
- if (element.isSubpathStart())
- internalHull.moveTo(sp);
- if (element.isLine()) {
- internalHull.lineTo(ep);
- lineSegments.append(QLineF(sp, ep));
- } else if (element.isConvex()) {
- internalHull.lineTo(ep);
- quadraticCurvesConvex.append(QPolygonF() << sp << cp << ep);
- } else {
- internalHull.lineTo(cp);
- internalHull.lineTo(ep);
- quadraticCurvesConcave.append(QPolygonF() << sp << cp << ep);
- }
- });
-
- QTriangleSet triangles = qTriangulate(internalHull);
-
- // Add triangles for curves. Note: These have to be adapted to 1/32 grid, since this
- // is the resolution of the triangulation
- QVarLengthArray<quint32> extraIndices;
- for (const QPolygonF &quadraticCurve : quadraticCurvesConvex) {
- QPointF v1 = quadraticCurve.at(0);
- QPointF v2 = quadraticCurve.at(1);
- QPointF v3 = quadraticCurve.at(2);
-
- extraIndices.append(triangles.vertices.size() / 2);
- triangles.vertices.append(qRound(v1.x() * 32.0) / 32.0);
- triangles.vertices.append(qRound(v1.y() * 32.0) / 32.0);
-
- extraIndices.append(triangles.vertices.size() / 2);
- triangles.vertices.append(qRound(v2.x() * 32.0) / 32.0);
- triangles.vertices.append(qRound(v2.y() * 32.0) / 32.0);
-
- extraIndices.append(triangles.vertices.size() / 2);
- triangles.vertices.append(qRound(v3.x() * 32.0) / 32.0);
- triangles.vertices.append(qRound(v3.y() * 32.0) / 32.0);
- }
-
- int startConcaveCurves = triangles.vertices.size() / 2;
- for (const QPolygonF &quadraticCurve : quadraticCurvesConcave) {
- QPointF v1 = quadraticCurve.at(0);
- QPointF v2 = quadraticCurve.at(1);
- QPointF v3 = quadraticCurve.at(2);
-
- extraIndices.append(triangles.vertices.size() / 2);
- triangles.vertices.append(qRound(v1.x() * 32.0) / 32.0);
- triangles.vertices.append(qRound(v1.y() * 32.0) / 32.0);
-
- extraIndices.append(triangles.vertices.size() / 2);
- triangles.vertices.append(qRound(v2.x() * 32.0) / 32.0);
- triangles.vertices.append(qRound(v2.y() * 32.0) / 32.0);
-
- extraIndices.append(triangles.vertices.size() / 2);
- triangles.vertices.append(qRound(v3.x() * 32.0) / 32.0);
- triangles.vertices.append(qRound(v3.y() * 32.0) / 32.0);
- }
-
- ret.append(addLoopBlinnNodes(triangles,
- extraIndices,
- startConcaveCurves,
- pathData,
- debugNodes));
- return ret;
-}
-
-QSGGeometryNode *QQuickShapeCurveRenderer::addLoopBlinnNodes(const QTriangleSet &triangles,
- const QVarLengthArray<quint32> &extraIndices,
- int startConcaveCurves,
- const PathData &pathData, NodeList *debugNodes)
-{
- const QColor &color = pathData.fillColor;
-
- // Basically we here need a way to determine if each triangle is:
- // 1. A convex curve
- // 2. A concave curve
- // 3. A filled triangle
- //
- // For filled triangles, we make all texture coordinates (1.0, 0.0)
- // For curves, we use (0, 0), (0.5, 0), (1, 1)
- // We use a third texture coordinate for curves: 0 means convex curve and 1 means concave
-
- QQuickShapeLoopBlinnNode *node = new QQuickShapeLoopBlinnNode(pathData.gradientType);
- node->fillGradient = pathData.gradient;
- node->color = color;
-
- if (pathData.isStrokeVisible() && pathData.useFragmentShaderStroker()) {
- node->strokeColor = pathData.pen.color();
- node->strokeWidth = pathData.pen.widthF();
- }
-
- QSGGeometry *g = node->geometry();
-
- QSGGeometry::Type indexType = triangles.indices.type() == QVertexIndexVector::UnsignedInt
- ? QSGGeometry::UnsignedIntType
- : QSGGeometry::UnsignedShortType;
- Q_ASSERT(indexType == QSGGeometry::UnsignedIntType); // Needs some code to support shorts
- // since extraIndices is int32
-
- QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertices;
- for (int i = 0; i < triangles.vertices.size(); i += 2) {
- QVector2D v(triangles.vertices.at(i), triangles.vertices.at(i + 1));
- QVector3D uv(0.0f, 1.0f, -1.0f);
- bool visualizeDebug = debugVisualization() & DebugCurves;
- vertices.append( { v.x(), v.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, visualizeDebug ? 0.5f : 0.0f });
- }
-
- for (int i = 0; i < extraIndices.size(); ++i) {
- int idx = extraIndices.at(i);
-
- QVector2D uv;
- switch (i % 3) {
- case 0:
- uv = QVector2D(0.0f, 0.0f);
- break;
- case 1:
- uv = QVector2D(0.5f, 0.0f);
- break;
- case 2:
- uv = QVector2D(1.0f, 1.0f);
- break;
}
- vertices[idx].u = uv.x();
- vertices[idx].v = uv.y();
- vertices[idx].w = idx >= startConcaveCurves ? 1.0f : -1.0f;
- vertices[idx].r = idx >= startConcaveCurves ? 0.0f : 1.0f;
- vertices[idx].g = 0.0f;
- vertices[idx].b = idx >= startConcaveCurves ? 1.0f : 0.0f;
- }
-
- if (g->indexType() != indexType) {
- g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
- vertices.size(),
- triangles.indices.size() + extraIndices.size(),
- indexType);
- node->setGeometry(g);
- } else {
- g->allocate(vertices.size(), triangles.indices.size() + extraIndices.size());
- }
-
- g->setDrawingMode(QSGGeometry::DrawTriangles);
- memcpy(g->vertexData(),
- vertices.data(),
- g->vertexCount() * g->sizeOfVertex());
- memcpy(g->indexData(),
- triangles.indices.data(),
- triangles.indices.size() * g->sizeOfIndex());
- memcpy((uchar*)g->indexData() + triangles.indices.size() * g->sizeOfIndex(),
- extraIndices.data(),
- extraIndices.size() * g->sizeOfIndex());
-
- m_rootNode->appendChildNode(node);
-
- if (debugVisualization() & DebugWireframe) {
- QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
- QSGGeometry *wfg = wfNode->geometry();
-
- QVarLengthArray<quint32> indices;
- QVarLengthArray<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
- for (int i = 0; i < triangles.indices.size() + extraIndices.size(); ++i) {
- quint32 index = i < triangles.indices.size()
- ? ((quint32*)triangles.indices.data())[i]
- : extraIndices[i - triangles.indices.size()];
-
- const QQuickShapeLoopBlinnNode::LoopBlinnVertex &vertex = vertices.at(index);
-
- float u = i % 3 == 0 ? 1.0f : 0.0f;
- float v = i % 3 == 1 ? 1.0f : 0.0f;
- float w = i % 3 == 2 ? 1.0f : 0.0f;
-
- indices.append(i);
- wfVertices.append({ vertex.x, vertex.y, u, v, w });
+ if (dirtyFlags & StrokeDirty) {
+ deleteAndClear(&pathData.strokeNodes);
+ deleteAndClear(&pathData.strokeDebugNodes);
+ if (pathData.isStrokeVisible()) {
+ const QPen &pen = pathData.pen;
+ if (pen.style() == Qt::SolidLine)
+ pathData.strokePath = pathData.path;
+ else
+ pathData.strokePath = pathData.path.dashed(pen.widthF(), pen.dashPattern(), pen.dashOffset());
+
+ if (useTriangulatingStroker)
+ pathData.strokeNodes = addTriangulatingStrokerNodes(pathData, &pathData.strokeDebugNodes);
+ else
+ pathData.strokeNodes = addCurveStrokeNodes(pathData, &pathData.strokeDebugNodes);
+ }
}
- if (wfg->indexType() != indexType) {
- wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
- wfVertices.size(),
- indices.size(),
- indexType);
- wfNode->setGeometry(wfg);
- } else {
- wfg->allocate(wfVertices.size(), indices.size());
+ if (dirtyFlags & UniformsDirty) {
+ if (!(dirtyFlags & FillDirty)) {
+ for (auto &pathNode : std::as_const(pathData.fillNodes))
+ static_cast<QQuickShapeCurveNode *>(pathNode)->setColor(pathData.fillColor);
+ }
+ if (!(dirtyFlags & StrokeDirty)) {
+ for (auto &strokeNode : std::as_const(pathData.strokeNodes))
+ static_cast<QQuickShapeCurveNode *>(strokeNode)->setColor(pathData.pen.color());
+ }
}
- wfg->setDrawingMode(QSGGeometry::DrawTriangles);
- memcpy(wfg->indexData(),
- indices.data(),
- indices.size() * g->sizeOfIndex());
- memcpy(wfg->vertexData(),
- wfVertices.data(),
- wfg->vertexCount() * wfg->sizeOfVertex());
-
- m_rootNode->appendChildNode(wfNode);
- debugNodes->append(wfNode);
+ pathData.m_dirty &= ~(PathDirty | FillDirty | StrokeDirty | UniformsDirty);
}
-
- return node;
}
// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space.
@@ -1421,19 +989,18 @@ void iteratePath(const QuadPath &path, int index, Func &&lambda)
iteratePath(path, element.indexOfChild(i), lambda);
}
}
-QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(const PathData &pathData, NodeList *debugNodes)
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData &pathData, NodeList *debugNodes)
{
//qDebug() << "========= STARTING ===========" << pathData.path;
-
+ auto *node = new QQuickShapeCurveNode;
+ node->setGradientType(pathData.gradientType);
QVector<QSGGeometryNode *> ret;
const QColor &color = pathData.fillColor;
QPainterPath internalHull;
- internalHull.setFillRule(pathData.path.fillRule());
+ internalHull.setFillRule(pathData.fillPath.fillRule());
- QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexBuffer;
- QVector<quint32> indices;
bool visualizeDebug = debugVisualization() & DebugCurves;
const float dbg = visualizeDebug ? 0.5f : 0.0f;
QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
@@ -1455,9 +1022,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f };
};
- auto addVertexDataForPoint = [&vertexBuffer, dbg](const QuadPath::Element &element, const QVector2D &p) {
- auto uv = element.uvForPoint(p);
-
+ auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp) {
float r = 0.0f, g = 0.0f, b = 0.0f;
if (element.isLine()) {
g = b = 1.0f;
@@ -1467,15 +1032,12 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
b = 1.0;
}
- vertexBuffer.append({p.x(), p.y(), uv.x(), uv.y(), uv.z(), r, g, b, dbg});
- };
+ QVector4D dbgColor(r, g, b, dbg);
+
+ node->appendTriangle(sp, cp, ep,
+ [&element](QVector2D v) { return element.uvForPoint(v); },
+ dbgColor, dbgColor, dbgColor);
- auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
- int currentVertex = vertexBuffer.count();
- addVertexDataForPoint(element, sp);
- addVertexDataForPoint(element, cp);
- addVertexDataForPoint(element, ep);
- indices << currentVertex << currentVertex + 1 << currentVertex + 2;
wfVertices.append({sp.x(), sp.y(), 1.0f, 0.0f, 0.0f}); // 0
wfVertices.append({cp.x(), cp.y(), 0.0f, 1.0f, 0.0f}); // 1
wfVertices.append({ep.x(), ep.y(), 0.0f, 0.0f, 1.0f}); // 2
@@ -1495,38 +1057,27 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
};
auto addLineTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
- int currentVertex = vertexBuffer.count();
addCurveTriangle(element, sp, ep, cp);
// Add a triangle on the outer side of the line to get some more AA
- // The new point replaces cp (currentVertex+1)
+ // The new point replaces cp
QVector2D op = findPointOtherSide(sp, ep, cp); //sp - (cp - ep);
- wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f}); // replacing cp (1)
- auto uv = element.uvForPoint(op);
- vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
- indices << currentVertex << currentVertex + 3 << currentVertex + 2;
+ addCurveTriangle(element, sp, op, ep);
};
auto addConvexTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
- int currentVertex = vertexBuffer.count();
addCurveTriangle(element, sp, ep, cp);
// Add two triangles on the outer side to get some more AA
- // First triangle on the line sp-cp, replacing ep (currentVertex+2)
+ // First triangle on the line sp-cp, replacing ep
{
- QVector2D op = findPointOtherSide(sp, cp, ep); //sp - (ep - cp);
- wfVertices.append({op.x(), op.y(), 0.0f, 0.0f, 1.0f}); // replacing ep (2)
- auto uv = element.uvForPoint(op);
- vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
- indices << currentVertex << currentVertex + 1 << currentVertex + 3;
+ QVector2D op = findPointOtherSide(sp, cp, ep); //sp - (ep - cp);
+ addCurveTriangle(element, sp, cp, op);
}
- // Second triangle on the line ep-cp, replacing sp (currentVertex+0)
+ // Second triangle on the line ep-cp, replacing sp
{
- QVector2D op = findPointOtherSide(ep, cp, sp); //ep - (sp - cp);
- wfVertices.append({op.x(), op.y(), 1.0f, 0.0f, 0.0f}); // replacing sp (0)
- auto uv = element.uvForPoint(op);
- vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
- indices << currentVertex + 4 << currentVertex + 1 << currentVertex + 2;
+ QVector2D op = findPointOtherSide(ep, cp, sp); //ep - (sp - cp);
+ addCurveTriangle(element, op, cp, ep);
}
};
@@ -1541,24 +1092,20 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
// Identical to addLineTriangle, except for how op is calculated
auto addConcaveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
- int currentVertex = vertexBuffer.count();
addCurveTriangle(element, sp, ep, cp);
// Add an outer triangle to give extra AA for very flat curves
QVector2D op = oppositePoint(sp, ep, cp);
- // The new point replaces cp (currentVertex+1)
- wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f}); // replacing cp (1)
- auto uv = element.uvForPoint(op);
- vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
- indices << currentVertex << currentVertex + 3 << currentVertex + 2;
+ // The new point replaces cp
+ addCurveTriangle(element, sp, op, ep);
};
auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3){
- int currentVertex = vertexBuffer.count();
- constexpr QVector3D uv(0.0, 1.0, -1.0);
- vertexBuffer.append({p1.x(), p1.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, dbg});
- vertexBuffer.append({p2.x(), p2.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, dbg});
- vertexBuffer.append({p3.x(), p3.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, dbg});
- indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ constexpr QVector3D uv(0.0, 1.0, -1.0);
+ QVector4D dbgColor(0.0f, 1.0f, 0.0f, dbg);
+ node->appendTriangle(p1, p2, p3,
+ [&uv](QVector2D) { return uv; },
+ dbgColor, dbgColor, dbgColor);
+
wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0
wfVertices.append({p3.x(), p3.y(), 0.0f, 1.0f, 0.0f}); // 1
wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f}); // 2
@@ -1566,8 +1113,8 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
- for (int i = 0; i < pathData.path.elementCount(); ++i)
- iteratePath(pathData.path, i, [&](const QuadPath::Element &element, int index){
+ for (int i = 0; i < pathData.fillPath.elementCount(); ++i)
+ iteratePath(pathData.fillPath, i, [&](const QuadPath::Element &element, int index){
QPointF sp(element.startPoint().toPointF()); //### to much conversion to and from pointF
QPointF cp(element.controlPoint().toPointF());
QPointF ep(element.endPoint().toPointF());
@@ -1632,7 +1179,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
for (int i = 0; i < 3; ++i) {
if (auto found = linePointHash.constFind(makeHashable(p[i])); found != linePointHash.constEnd()) {
// check if this triangle is on a line, i.e. if one point is the sp and another is the ep of the same path element
- const auto &element = pathData.path.elementAt(*found);
+ const auto &element = pathData.fillPath.elementAt(*found);
//qDebug() << " " << element;
for (int j = 0; j < 3; ++j) {
if (i != j && roundVec2D(element.endPoint()) == p[j]) {
@@ -1649,7 +1196,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
// check if this triangle is on the tangent line of a concave curve,
// i.e if one point is the cp, and the other is sp or ep
// TODO: clean up duplicated code (almost the same as the lineElement path above)
- const auto &element = pathData.path.elementAt(*found);
+ const auto &element = pathData.fillPath.elementAt(*found);
for (int j = 0; j < 3; ++j) {
if (i == j)
continue;
@@ -1666,7 +1213,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
}
} else if (auto found = convexPointHash.constFind(makeHashable(p[i])); found != convexPointHash.constEnd()) {
// check if this triangle is on a curve, i.e. if one point is the sp and another is the ep of the same path element
- const auto &element = pathData.path.elementAt(*found);
+ const auto &element = pathData.fillPath.elementAt(*found);
for (int j = 0; j < 3; ++j) {
if (i != j && roundVec2D(element.endPoint()) == p[j]) {
if (foundElement)
@@ -1682,13 +1229,13 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
}
if (lineElementIndex != -1) {
int ci = (6 - si - ei) % 3; // 1+2+3 is 6, so missing number is 6-n1-n2
- addLineTriangle(pathData.path.elementAt(lineElementIndex), p[si], p[ei], p[ci]);
+ addLineTriangle(pathData.fillPath.elementAt(lineElementIndex), p[si], p[ei], p[ci]);
} else if (concaveElementIndex != -1) {
- addCurveTriangle(pathData.path.elementAt(concaveElementIndex), p[0], p[1], p[2]);
+ addCurveTriangle(pathData.fillPath.elementAt(concaveElementIndex), p[0], p[1], p[2]);
} else if (convexElementIndex != -1) {
int oi = (6 - si - ei) % 3;
const auto &otherPoint = p[oi];
- const auto &element = pathData.path.elementAt(convexElementIndex);
+ const auto &element = pathData.fillPath.elementAt(convexElementIndex);
// We have to test whether the triangle can cross the line TODO: use the toplevel element's safe space
bool safeSpace = pointInSafeSpace(otherPoint, element);
if (safeSpace) {
@@ -1747,34 +1294,18 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
}
}
+ QVector<quint32> indices = node->uncookedIndexes();
if (indices.size() > 0) {
- auto *node = new QQuickShapeLoopBlinnNode(pathData.gradientType);
- node->color = color;
- node->fillGradient = pathData.gradient;
-
- if (pathData.useFragmentShaderStroker() && pathData.isStrokeVisible() && pathData.pen.isSolid()) {
- node->strokeColor = pathData.pen.color();
- node->strokeWidth = pathData.pen.widthF();
- }
-
- QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
- vertexBuffer.size(), indices.size(), QSGGeometry::UnsignedIntType);
- node->setGeometry(g);
- g->setDrawingMode(QSGGeometry::DrawTriangles);
-
- //qDebug() << "gvc" << g->vertexCount();
-
- memcpy(g->vertexData(),
- vertexBuffer.data(),
- g->vertexCount() * g->sizeOfVertex());
- memcpy(g->indexData(),
- indices.data(),
- indices.size() * g->sizeOfIndex());
+ node->setColor(color);
+ node->setFillGradient(pathData.gradient);
+ node->cookGeometry();
m_rootNode->appendChildNode(node);
ret.append(node);
}
- const bool wireFrame = debugVisualization() & DebugWireframe;
+
+
+ const bool wireFrame = debugVisualization() & DebugWireframe;
if (wireFrame) {
QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
@@ -1801,22 +1332,24 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(cons
return ret;
}
-QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathData &pathData, NodeList *debugNodes)
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNodes(const PathData &pathData, NodeList *debugNodes)
{
QVector<QSGGeometryNode *> ret;
const QColor &color = pathData.pen.color();
- QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexBuffer;
- QVector<quint32> indices;
bool visualizeDebug = debugVisualization() & DebugCurves;
const float dbg = visualizeDebug ? 0.5f : 0.0f;
QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
QTriangulatingStroker stroker;
- const QVectorPath &vp = qtVectorPathForPath(pathData.fillPath);
+ const auto painterPath = pathData.strokePath.toPainterPath();
+ const QVectorPath &vp = qtVectorPathForPath(painterPath);
QPen pen = pathData.pen;
stroker.process(vp, pen, {}, {});
+ auto *node = new QQuickShapeCurveNode;
+ node->setGradientType(pathData.gradientType);
+
auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){
QVector2D baseLine = endPoint - startPoint;
@@ -1832,7 +1365,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathDa
static bool disableExtraTriangles = qEnvironmentVariableIntValue("QT_QUICKSHAPES_WIP_DISABLE_EXTRA_STROKE_TRIANGLES");
auto addStrokeTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, bool){
- int currentVertex = vertexBuffer.count();
//constexpr QVector3D uv(0.0, 1.0, -1.0);
@@ -1841,11 +1373,18 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathDa
return;
}
+ auto uvForPoint = [&p1, &p2, &p3](QVector2D p) {
+ auto uv = curveUv(p1, p2, p3, p);
+ return QVector3D(uv.x(), uv.y(), 0.0f); // Line
+ };
+
+ node->appendTriangle(p1, p2, p3,
+ uvForPoint,
+ QVector4D(1.0f, 0.0, 0.0, dbg),
+ QVector4D(0.0f, 1.0, 0.0, dbg),
+ QVector4D(0.0f, 0.0, 1.0, dbg));
+
- vertexBuffer.append({p1.x(), p1.y(), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, dbg}); // edge start
- vertexBuffer.append({p2.x(), p2.y(), 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, dbg}); // inner
- vertexBuffer.append({p3.x(), p3.y(), 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, dbg}); // edge end
- indices << currentVertex << currentVertex + 1 << currentVertex + 2;
wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0
wfVertices.append({p2.x(), p2.y(), 0.0f, 0.1f, 0.0f}); // 1
wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f}); // 2
@@ -1854,9 +1393,15 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathDa
// Add a triangle on the outer side of the line to get some more AA
// The new point replaces p2 (currentVertex+1)
QVector2D op = findPointOtherSide(p1, p3, p2);
+ node->appendTriangle(p1, op, p3,
+ uvForPoint,
+ QVector4D(1.0f, 0.0, 0.0, dbg),
+ QVector4D(1.0f, 1.0, 0.0, dbg),
+ QVector4D(0.0f, 0.0, 1.0, dbg));
+
+ wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f});
wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f}); // replacing p2
- vertexBuffer.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, dbg});
- indices << currentVertex << currentVertex + 3 << currentVertex + 2;
+ wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f});
}
};
@@ -1871,30 +1416,12 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathDa
addStrokeTriangle(p[0], p[1], p[2], isOdd);
}
+ QVector<quint32> indices = node->uncookedIndexes();
if (indices.size() > 0) {
- auto *node = new QQuickShapeLoopBlinnNode(pathData.gradientType);
- node->color = color;
- node->fillGradient = pathData.gradient;
-
- if (pathData.useFragmentShaderStroker() && pathData.isStrokeVisible() && pathData.pen.isSolid()) {
- node->strokeColor = pathData.pen.color();
- node->strokeWidth = pathData.pen.widthF();
- }
-
- QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
- vertexBuffer.size(), indices.size(), QSGGeometry::UnsignedIntType);
- node->setGeometry(g);
- g->setDrawingMode(QSGGeometry::DrawTriangles);
-
- //qDebug() << "gvc" << g->vertexCount();
-
- memcpy(g->vertexData(),
- vertexBuffer.data(),
- g->vertexCount() * g->sizeOfVertex());
- memcpy(g->indexData(),
- indices.data(),
- indices.size() * g->sizeOfIndex());
+ node->setColor(color);
+ node->setFillGradient(pathData.gradient);
+ node->cookGeometry();
m_rootNode->appendChildNode(node);
ret.append(node);
}
@@ -1925,7 +1452,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathDa
return ret;
}
-void QQuickShapeCurveRenderer::setRootNode(QQuickShapeCurveNode *node)
+void QQuickShapeCurveRenderer::setRootNode(QSGNode *node)
{
m_rootNode = node;
}
@@ -1964,7 +1491,7 @@ namespace {
triangles are colliding.
*/
-// The sign of the determinant tells the winding order
+// The sign of the determinant tells the winding order: positive means counter-clockwise
static inline double determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
{
return p1.x() * (p2.y() - p3.y())
@@ -2081,15 +1608,18 @@ struct TriangleData
QVector2D points[3];
int pathElementIndex;
QVector3D debugColor;
- bool specialDebug = false;
+ bool specialDebug = false; // Quick way of debugging special cases without having to change debugColor
};
+// Returns a vector that is normal to baseLine, pointing to the right
QVector2D normalVector(QVector2D baseLine)
{
QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
return normal;
}
+// Returns a vector that is normal to the path and pointing to the right. If endSide is false
+// the vector is normal to the start point, otherwise to the end point
QVector2D normalVector(const QuadPath::Element &element, bool endSide = false)
{
if (element.isLine())
@@ -2100,11 +1630,30 @@ QVector2D normalVector(const QuadPath::Element &element, bool endSide = false)
return normalVector(element.endPoint() - element.controlPoint());
}
-QVector2D angleBisector(const QuadPath::Element &element1, const QuadPath::Element &element2, float strokeMargin)
+// Returns a vector that is parallel to the path. If endSide is false
+// the vector starts at the start point and points forward,
+// otherwise it starts at the end point and points backward
+QVector2D tangentVector(const QuadPath::Element &element, bool endSide = false)
{
- Q_ASSERT(element1.endPoint() == element2.startPoint());
+ if (element.isLine()) {
+ if (!endSide)
+ return element.endPoint() - element.startPoint();
+ else
+ return element.startPoint() - element.endPoint();
+ } else {
+ if (!endSide)
+ return element.controlPoint() - element.startPoint();
+ else
+ return element.controlPoint() - element.endPoint();
+ }
+}
+QVector2D miterBisector(const QuadPath::Element &element1, const QuadPath::Element &element2, float strokeMargin, float inverseMiterLimit,
+ bool *ok = nullptr, bool *pointingRight = nullptr)
+{
+ Q_ASSERT(element1.endPoint() == element2.startPoint());
+
const auto p1 = element1.isLine() ? element1.startPoint() : element1.controlPoint();
const auto p2 = element1.endPoint();
const auto p3 = element2.isLine() ? element2.endPoint() : element2.controlPoint();
@@ -2118,6 +1667,10 @@ QVector2D angleBisector(const QuadPath::Element &element1, const QuadPath::Eleme
// angle bisector formula will give an almost null vector: use normal of bisector of normals instead
QVector2D n1(-v1.y(), v1.x());
QVector2D n2(-v2.y(), v2.x());
+ if (ok)
+ *ok = true;
+ if (pointingRight)
+ *pointingRight = true;
return (n2 - n1).normalized() * strokeMargin;
} else {
// We need to increase the length, so that the result covers the whole miter
@@ -2128,460 +1681,351 @@ QVector2D angleBisector(const QuadPath::Element &element1, const QuadPath::Eleme
float cos2x = QVector2D::dotProduct(v1, v2);
cos2x = qMin(1.0f, cos2x); // Allow for float inaccuracy
float sine = sqrt((1.0f - cos2x) / 2);
- sine = qMax(sine, 0.1f); // Avoid divide by zero
-
+ if (ok)
+ *ok = sine >= inverseMiterLimit / 2.0f;
+ if (pointingRight)
+ *pointingRight = determinant(p1, p2, p3) > 0;
+ sine = qMax(sine, 0.01f); // Avoid divide by zero
return bisector.normalized() * strokeMargin / sine;
- // TODO: Make a proper bevel at that point
- // ### This is *not* the right place to do a miter limit: The inner join needs to be as long as
- // it needs to be, independent of the miter on the other side. (We still need to avoid a divide
- // by zero, of course.)
}
}
-// Returns true if p and q are on the same side of the line AB
-bool onSameSideOfLine(const QVector2D &a, const QVector2D &b, const QVector2D &p, const QVector2D &q)
-{
- auto baseLine = a - b;
- QVector2D n(-baseLine.y(), baseLine.x());
- float pSide = testSideOfLineByNormal(a, n, p);
- float qSide = testSideOfLineByNormal(a, n, q);
- return pSide * qSide >= 0;
-}
-
-// Returns true if the entire curve triangle is on the same side of the line p1-p2
-bool controlPointInsideLine(const QuadPath::Element &element, const QVector2D &p1, const QVector2D &p2)
-{
- bool s1 = determinant(p1, p2, element.startPoint()) < 0;
- auto s2 = determinant(p1, p2, element.endPoint()) < 0;
- auto s3 = determinant(p1, p2, element.controlPoint()) < 0;
- // If all determinants have the same sign, all the points are on the same side
- return (s1 == s2 && s2 == s3);
-}
-
-inline bool lineIntersects(const QVector2D &v1_, const QVector2D &v2_,
- const QVector2D &u1_, const QVector2D &u2_)
-{
-#if 1
- QPointF v1 = v1_.toPointF();
- QPointF v2 = v2_.toPointF();
- QPointF u1 = u1_.toPointF();
- QPointF u2 = u2_.toPointF();
-
- if ( (v1.x() < u1.x() && v1.x() < u2.x() && v2.x() < u1.x() && v2.x() < u2.x())
- || (v1.x() > u1.x() && v1.x() > u2.x() && v2.x() > u1.x() && v2.x() > u2.x())
- || (v1.y() < u1.y() && v1.y() < u2.y() && v2.y() < u1.y() && v2.y() < u2.y())
- || (v1.y() > u1.y() && v1.y() > u2.y() && v2.y() > u1.y() && v2.y() > u2.y())) {
- return false;
- }
-
- // Copied from QLineF::intersects() with some changes to ordering to speed
- // up for our use case
- const QPointF a = v2 - v1;
- const QPointF b = u1 - u2;
- const QPointF c = v1 - u1;
-
- const qreal denominator = a.y() * b.x() - a.x() * b.y();
- if (denominator == 0.0 || !qt_is_finite(denominator))
- return false;
-
- const qreal reciprocal = 1.0 / denominator;
- qreal na = (b.y() * c.x() - b.x() * c.y()) * reciprocal;
- if (na < 0.0 || na > 1.0)
- return false;
-
- qreal nb = (a.x() * c.y() - a.y() * c.x()) * reciprocal;
- if (nb < 0.0 || nb > 1.0)
- return false;
-
- const QPointF intersectionPoint = v1 + a * na;
- return intersectionPoint != v1 && intersectionPoint != v2 && intersectionPoint != u1 && intersectionPoint != u2;
- return true;
-#else
- QLineF v(v1.toPointF(), v2.toPointF());
- QLineF u(u1.toPointF(), u2.toPointF());
-
- QPointF intersectionPoint;
- if (v.intersects(u, &intersectionPoint) == QLineF::BoundedIntersection) {
- return (intersectionPoint != v.p1() && intersectionPoint != v.p2()
- && intersectionPoint != u.p1() && intersectionPoint != u.p2());
- }
-
- return false;
-#endif
-}
-
-
-static QList<TriangleData> customTriangulator(const QuadPath &path, float margin, bool miterJoin)
+// Really simplistic O(n^2) triangulator - only intended for five points
+QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, int elementIndex)
{
+ int count = pts.size();
+ Q_ASSERT(count >= 3);
QList<TriangleData> ret;
- // TODO: We repeat the same calculations for each side of each join: try to remember the values from the previous
- // element. (The difficult part is path closures)
-
- auto handleElement = [&ret, margin, miterJoin](const QuadPath::Element *prev, const QuadPath::Element *current, const QuadPath::Element *next, const int elementIndex) {
- QVector2D p1, p2, p3, p4;
-
-
- auto startNormal = normalVector(*current) * margin;
- auto endNormal = normalVector(*current, true) * margin;
- auto startBisector = prev ? angleBisector(*prev, *current, margin) : startNormal;
- auto endBisector = next ? angleBisector(*current, *next, margin) : endNormal;
-
- const auto &sp = current->startPoint();
- const auto &ep = current->endPoint();
+ ret.append({{pts[0], pts[1], pts[2]}, elementIndex, {1, 0, 0}});
- // p1: bisect on the "inside" of the join, p2: normal on the "outside"
- // guess which side is which: (TODO: we should be able to know this)
- bool bevelPointReversed = false;
-
- if (prev) {
- const auto &prevPoint = prev->isLine() ? prev->startPoint() : prev->controlPoint();
- const auto &lineStart = current->startPoint();
- const auto &lineEnd = current->isLine() ? current->endPoint() : current->controlPoint();
- if (miterJoin) {
- p1 = sp + startBisector;
- p2 = sp - startBisector;
- } else {
- p1 = sp - startBisector;
- p2 = sp - startNormal;
- // p1 should be on the same side as the start point of the previous element
- if (!onSameSideOfLine(lineStart, lineEnd, p1, prevPoint)) {
- p1 = sp + startBisector;
- }
- // p2 should be on the opposite side ### TODO: optimize test, reusing intermediates from test above
- if (onSameSideOfLine(lineStart, lineEnd, p2, prevPoint)) {
- p2 = sp + startNormal;
- bevelPointReversed = true;
- }
- }
- } else {
- p1 = sp + startNormal;
- p2 = sp - startNormal;
+ // hull is always in positive determinant winding order
+ QList<QVector2D> hull;
+ float det1 = determinant(pts[0], pts[1], pts[2]);
+ if (det1 > 0)
+ hull << pts[0] << pts[1] << pts[2];
+ else
+ hull << pts[2] << pts[1] << pts[0];
+ auto connectableInHull = [&](const QVector2D &pt) -> QList<int> {
+ QList<int> r;
+ const int n = hull.size();
+ for (int i = 0; i < n; ++i) {
+ const auto &p1 = hull.at(i);
+ const auto &p2 = hull.at((i+1) % n);
+ if (determinant(p1, p2, pt) < 0.0f)
+ r << i;
}
- auto bevelPoint = p2; // This is one corner of the "gap" triangle
-
- // p3: bisect on the "inside" of the join, p4: normal on the "outside"
- // For curves, we use the tangent line of the next element
- if (next) {
- const auto &lineStart = current->isLine() ? current->startPoint() : current->controlPoint();
- const auto &lineEnd = current->endPoint();
- const auto &nextPoint = next->isLine() ? next->endPoint() : next->controlPoint();
- if (miterJoin) {
- p3 = ep + endBisector;
- p4 = ep - endBisector;
- } else {
- p3 = ep - endBisector;
- p4 = ep - endNormal;
- // p3 should be on the same side of our line as the end point of the next element
- if (!onSameSideOfLine(lineStart, lineEnd, p3, nextPoint)) {
- p3 = ep + endBisector;
- }
- // p4 should be on the opposite side ### TODO: optimize test, reusing intermediates from test above
- if (onSameSideOfLine(lineStart, lineEnd, p4, nextPoint)) {
- p4 = ep + endNormal;
- }
+ return r;
+ };
+ for (int i = 3; i < count; ++i) {
+ const auto &p = pts[i];
+ auto visible = connectableInHull(p);
+ if (visible.isEmpty())
+ continue;
+ int visCount = visible.count();
+ int hullCount = hull.count();
+ // Find where the visible part of the hull starts. (This is the part we need to triangulate to,
+ // and the part we're going to replace. "visible" contains the start point of the line segments that are visible from p.
+ int boundaryStart = visible[0];
+ for (int j = 0; j < visCount - 1; ++j) {
+ if ((visible[j] + 1) % hullCount != visible[j+1]) {
+ boundaryStart = visible[j + 1];
+ break;
}
- } else {
- p3 = ep + endNormal;
- p4 = ep - endNormal;
}
-
- //bool intersect = lineIntersects(p1, p2, p3, p4); // Check if our quadrilateral is self-intersecting
- //### Not enough! It's also wrong if p1-p2 is completely on the wrong side of p3-p4
- //bool ok = onSameSideOfLine(p1, p2, p3, p4) && onSameSideOfLine(p1, p2, p3, current->endPoint());
- //### And that's still not enough, since the line can turn almost 180 degrees
-
- // New attempt: see if the bisector intersects the normal...
- bool notOK = lineIntersects(sp + startNormal, sp - startNormal, ep + endBisector, ep - endBisector)
- || lineIntersects(sp + startBisector, sp - startBisector, ep + endNormal, ep - endNormal);
-
- // TESTING: Avoid the inner bisector extending past the stroke. We need to move the points around,
- // but for now, just accept overpainting. Only implemented for BevelJoin
- if (notOK && !miterJoin) {
- p1 = current->startPoint() + startNormal;
- p2 = current->startPoint() - startNormal;
- p3 = current->endPoint() + endNormal;
- p4 = current->endPoint() - endNormal;
+ // Now add those triangles
+ for (int j = 0; j < visCount; ++j) {
+ const auto &p1 = hull.at((boundaryStart + j) % hullCount);
+ const auto &p2 = hull.at((boundaryStart + j+1) % hullCount);
+ ret.append({{p1, p2, p}, elementIndex, {1,1,0}});
}
-
- if (current->isLine()) {
- // Rearrange so p1, p3 is on the same side of the path, and p2, p4 is on the other side
- if (!onSameSideOfLine(current->startPoint(), current->endPoint(), p1, p3))
- qSwap(p3, p4);
- } else {
- // Rearrange so that p1 and p3 are on the outside of the curve triangle, i.e. p2 and p4 are on the same side
- // of the respective tangent line as the path
-
-
- // p1 should be on the outside of sp-cp
- if (onSameSideOfLine(current->startPoint(), current->controlPoint(), p1, current->endPoint()))
- qSwap(p1, p2);
- // p3 should be on the outside of ep-cp
- if (onSameSideOfLine(current->endPoint(), current->controlPoint(), p3, current->startPoint()))
- qSwap(p3, p4);
+ // Finally replace the points that are now inside the hull
+ // We insert p after boundaryStart, and before boundaryStart + visCount (modulo...)
+ // and remove the points inbetween
+ int pointsToKeep = hullCount - visCount + 1;
+ QList<QVector2D> newHull;
+ newHull << p;
+ for (int j = 0; j < pointsToKeep; ++j) {
+ newHull << hull.at((j + boundaryStart + visCount) % hullCount);
}
+ hull = newHull;
+ }
+ return ret;
+}
- bool special = notOK;
+static QList<TriangleData> customTriangulator2(const QuadPath &path, float penWidth, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, float miterLimit)
+{
+ const bool bevelJoin = joinStyle == Qt::BevelJoin;
+ const bool roundJoin = joinStyle == Qt::RoundJoin;
+ const bool miterJoin = !bevelJoin && !roundJoin;
- /*
- p
- p1 p3
+ const bool roundCap = capStyle == Qt::RoundCap;
+ const bool squareCap = capStyle == Qt::SquareCap;
- sp ep
+ Q_ASSERT(miterLimit > 0 || !miterJoin);
+ float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0;
- p2 p4
- */
+ static const int additionalSpace = qEnvironmentVariableIntValue("QT_QUICKSHAPES_EXTRA_SPACE");
- //Note: p1-sp-p2 is no longer guaranteed to be on a line, so we need to add extra triangles
+ const float extraFactor = roundJoin && additionalSpace ? (penWidth + additionalSpace) / penWidth : 2.0;
- if (current->isLine() || controlPointInsideLine(*current, p1, p3)) {
- float r = current->isLine() ? 0.0f : 0.5f;
- ret.append({{p1, p2, p3}, elementIndex, {r, 1.0f, 0.0f}, special});
- ret.append({{p2, p3, p4}, elementIndex, {r, 0.0f, 1.0f}, special});
- if (!miterJoin) { // TODO: handle bevel for overlong miter
- ret.append({{p1, sp, p2}, elementIndex, {r, 0.5f, 1.0f}, special});
- ret.append({{p3, ep, p4}, elementIndex, {r, 0.5f, 1.0f}, special});
+ QList<TriangleData> ret;
+ int count = path.elementCount();
+ int subStart = 0;
+ while (subStart < count) {
+ int subEnd = subStart;
+ for (int i = subStart + 1; i < count; ++i) {
+ const auto &e = path.elementAt(i);
+ if (e.isSubpathStart()) {
+ subEnd = i - 1;
+ break;
}
- //qDebug() << "adding triangles" << p1 << p2 << p3 << "and" << p2 << p3 << p4;
- } else {
-
- const auto &p = current->controlPoint();
- ret.append({{p1, p2, p}, elementIndex, {1.0f, 0.0f, 0.0f}, special});
- ret.append({{p, p2, p4}, elementIndex, {1.0f, 1.0f, 0.0f}, special});
- ret.append({{p, p3, p4}, elementIndex, {1.0f, 0.0f, 1.0f}, special});
- if (!miterJoin) { // TODO: handle bevel for overlong miter
- ret.append({{p1, sp, p2}, elementIndex, {0.5f, 1.0f, 0.0f}, special});
- ret.append({{p3, ep, p4}, elementIndex, {0.5f, 1.0f, 0.0f}, special});
+ if (i == count - 1) {
+ subEnd = i;
+ break;
}
- //qDebug() << "adding triangle1" << p1 << p2 << p << "triangle2" << p << p2 << p4 << "triangle3" << p << p3 << p4 ;
-
-
- // qDebug() << "Curve element" << p1 << p2 << p << p3 << p4;
- }
-
- if (prev && !miterJoin) {
- // Try to do the bevel triangle: prev bevelpoint - sp - bevelPoint
- auto prevNormal = normalVector(*prev, true) * margin; // Do we have normals in different directions???
- auto pbp = bevelPointReversed ? sp + prevNormal : sp - prevNormal;
- //qDebug() << "bevel triangle" << pbp << sp << bevelPoint;
- ret.append({{pbp, sp, bevelPoint}, -1, {}, true});
- }
- };
-
+ }
+ bool closed = path.elementAt(subStart).startPoint() == path.elementAt(subEnd).endPoint();
+ const int subCount = subEnd - subStart + 1;
+
+ auto elementAt = [&](int idx, int delta) -> const QuadPath::Element * {
+ int subIdx = idx - subStart;
+ if (closed) {
+ subIdx = (subIdx + subCount + delta) % subCount;
+ return &path.elementAt(subStart + subIdx);
+ }
+ subIdx += delta;
+ if (subIdx >= 0 && subIdx < subCount)
+ return &path.elementAt(subStart + subIdx);
+ return nullptr;
+ };
+
+ for (int i = subStart; i <= subEnd; ++i) {
+ const auto &element = path.elementAt(i);
+ const auto *nextElement = elementAt(i, +1);
+ const auto *prevElement = elementAt(i, -1);
+
+ const auto &s = element.startPoint();
+ const auto &c = element.controlPoint();
+ const auto &e = element.endPoint();
+
+ // Normals point to the right
+ QVector2D startNormal = normalVector(element).normalized() * (penWidth / 2);
+ QVector2D endNormal = normalVector(element, true).normalized() * (penWidth / 2);
+
+ // Bisectors point to the inside of the curve: make them point the same way as normals
+ bool startBisectorPointsRight = true;
+ bool startBisectorWithinMiterLimit = true;
+ QVector2D startBisector = prevElement ? miterBisector(*prevElement, element, penWidth / 2, inverseMiterLimit, &startBisectorWithinMiterLimit, &startBisectorPointsRight) : startNormal;
+ if (!startBisectorPointsRight)
+ startBisector = -startBisector;
+
+ bool endBisectorPointsRight = true;
+ bool endBisectorWithinMiterLimit = true;
+ QVector2D endBisector = nextElement ? miterBisector(element, *nextElement, penWidth / 2, inverseMiterLimit, &endBisectorWithinMiterLimit, &endBisectorPointsRight) : endNormal;
+ if (!endBisectorPointsRight)
+ endBisector = -endBisector;
+
+ // We can't use the simple miter for miter joins, since the shader currently only supports round joins
+ bool simpleMiter = joinStyle == Qt::RoundJoin;
+
+ // TODO: miterLimit
+ bool startMiter = simpleMiter && startBisectorWithinMiterLimit;
+ bool endMiter = simpleMiter && endBisectorWithinMiterLimit;
+
+ QVector2D p1, p2, p3, p4;
+ if (startMiter) {
+ //### bisector on inside can extend further than element: must limit it somehow
+ p1 = s + startBisector * extraFactor;
+ p2 = s - startBisector * extraFactor;
+ } else {
+ // TODO: remove the overdraw by using the intersection point on the inside (for lines, this is the bisector, but
+ // it's more complex for non-smooth curve joins)
- /*
- Iterate through the path. Each start element has to be postponed until the end of the sub-path.
- At that point, we have to process both the start and the end elements.
+ // For now, simple bevel: just use normals and live with overdraw
+ p1 = s + startNormal * extraFactor;
+ p2 = s - startNormal * extraFactor;
+ }
+ // repeat logic above for the other end:
+ if (endMiter) {
+ p3 = e + endBisector * extraFactor;
+ p4 = e - endBisector * extraFactor;
+ } else {
+ p3 = e + endNormal * extraFactor;
+ p4 = e - endNormal * extraFactor;
+ }
- If the end element's end point is equal to the start element's start point, we treat them as connected,
- otherwise we add null pointers
- */
+ // End caps
+
+ if (!prevElement) {
+ QVector2D capSpace = tangentVector(element).normalized() * -penWidth;
+ if (roundCap) {
+ p1 += capSpace;
+ p2 += capSpace;
+ } else if (squareCap) {
+ QVector2D c1 = p1 + capSpace;
+ QVector2D c2 = p2 + capSpace;
+ ret.append({{p1, s, c1}, -1, {1, 1, 0}, true});
+ ret.append({{c1, s, c2}, -1, {1, 1, 0}, true});
+ ret.append({{p2, s, c2}, -1, {1, 1, 0}, true});
+ }
+ }
+ if (!nextElement) {
+ QVector2D capSpace = tangentVector(element, true).normalized() * -penWidth;
+ if (roundCap) {
+ p3 += capSpace;
+ p4 += capSpace;
+ } else if (squareCap) {
+ QVector2D c3 = p3 + capSpace;
+ QVector2D c4 = p4 + capSpace;
+ ret.append({{p3, e, c3}, -1, {1, 1, 0}, true});
+ ret.append({{c3, e, c4}, -1, {1, 1, 0}, true});
+ ret.append({{p4, e, c4}, -1, {1, 1, 0}, true});
+ }
+ }
- const QuadPath::Element *startElement = nullptr;
- int startElementIndex = -1;
- const QuadPath::Element *prevElement = nullptr;
- //### For now, we know that this path has not been split
- for (int i = 0; i < path.elementCount(); ++i) {
- const auto *element = &path.elementAt(i);
- // ### isSubPathEnd() doesn't work unless subPathsClosed() has been called
- bool isSubPathEnd = i == path.elementCount() - 1 || path.elementAt(i + 1).isSubpathStart();
- bool isSubPathStart = element->isSubpathStart() || i == 0; // 0 should always be subPathStart, but just to be sure
- if (isSubPathStart) {
- if (isSubPathEnd) {
- // If the element is both start and end, it's a standalone
- handleElement(nullptr, element, nullptr, i);
- startElement = nullptr; // These should be ignored, but reset just in case
- prevElement = nullptr;
+ if (element.isLine()) {
+ ret.append({{p1, p2, p3}, i, {0,1,0}, false});
+ ret.append({{p2, p3, p4}, i, {0.5,1,0}, false});
} else {
- // this element will be handled later, when we know whether the sub-path is closed
- startElement = element;
- startElementIndex = i;
- prevElement = element;
+ bool controlPointOnRight = determinant(s, c, e) > 0;
+ QVector2D controlPointOffset = (startNormal + endNormal).normalized() * penWidth;
+ QVector2D p5 = controlPointOnRight ? c - controlPointOffset : c + controlPointOffset;
+ ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, i));
}
- continue;
- }
- const QuadPath::Element *nextElement = nullptr;
- if (i != 0 && !element->isSubpathStart())
- prevElement = &path.elementAt(i - 1);
-
- if (isSubPathEnd) {
- //handle the previous start element: check if it is connected to this element
- Q_ASSERT(startElement);
- bool subPathClosed = element->endPoint() == startElement->startPoint();
- handleElement(subPathClosed ? element : nullptr, startElement, &path.elementAt(startElementIndex + 1), startElementIndex);
- if (subPathClosed)
- nextElement = startElement;
- startElement = nullptr;
- } else {
- nextElement = &path.elementAt(i + 1);
+ if (!endMiter && nextElement) {
+ // inside of join (opposite of bevel) is defined by
+ // triangle s, e, next.e
+ QVector2D outer1, outer2;
+ const auto &np = nextElement->isLine() ? nextElement->endPoint() : nextElement->controlPoint();
+ bool innerOnRight = endBisectorPointsRight;
+ auto nextNormal = calcNormalVector(e, np).normalized() * penWidth / 2;
+
+ if (innerOnRight) {
+ outer1 = e - 2 * endNormal;
+ outer2 = e + 2 * nextNormal;
+ } else {
+ outer1 = e + 2 * endNormal;
+ outer2 = e - 2 * nextNormal;
+ }
+
+ if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) {
+ ret.append({{outer1, e, outer2}, -1, {1,1,0}, false});
+ } else if (roundJoin) {
+ ret.append({{outer1, e, outer2}, i, {1,1,0}, false});
+ QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penWidth / 2;
+ if (!innerOnRight)
+ nn = -nn;
+ ret.append({{outer1, outer1 + nn, outer2}, i, {1,1,0}, false});
+ ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {1,1,0}, false});
+
+ } else if (miterJoin) {
+ QVector2D outer = innerOnRight ? e - endBisector * 2 : e + endBisector * 2;
+ ret.append({{outer1, e, outer}, -2, {1,1,0}, false});
+ ret.append({{outer, e, outer2}, -2, {1,1,0}, false});
+ }
+ }
}
- handleElement(prevElement, element, nextElement, i);
- prevElement = isSubPathEnd ? nullptr : element;
+ subStart = subEnd + 1;
}
- Q_ASSERT(!startElement); // Should have been handled in the loop
-
return ret;
}
};
-QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addNodesStrokeShader(const PathData &pathData, NodeList *debugNodes)
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addCurveStrokeNodes(const PathData &pathData, NodeList *debugNodes)
{
QVector<QSGGeometryNode *> ret;
-
- Q_UNUSED(debugNodes);
-
const QColor &color = pathData.pen.color();
- Q_UNUSED(color)
-
- QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexBuffer;
- QVector<quint32> indices;
- bool visualizeDebug = debugVisualization() & DebugCurves;
- const float dbg = visualizeDebug ? 0.5f : 0.0f;
- QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
-
- const auto &thePath = pathData.qPath;
- // qDebug() << "Stroking" << thePath;
+ auto *node = new QQuickShapeStrokeNode;
+ QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
- // TODO: calculate margin correctly for narrow angles. If it's too big we'll get into unsafe space more often
- auto triangles = customTriangulator(thePath, float(pathData.pen.widthF()) * 1, pathData.pen.joinStyle() == Qt::MiterJoin); //### SvgMiterJoin ???
+ float miterLimit = pathData.pen.miterLimit();
+ float penWidth = pathData.pen.widthF();
- //### TODO: reduce code duplication
- auto addVertexDataForPoint = [&vertexBuffer, &wfVertices, dbg](const QuadPath::Element &element, const QVector2D &p, const QVector3D &color, int wfIndex) {
- auto uv = element.uvForPoint(p);
+ auto thePath = pathData.strokePath;
+ auto triangles = customTriangulator2(thePath, penWidth, pathData.pen.joinStyle(), pathData.pen.capStyle(), miterLimit);
- float r = color.x(), g = color.y(), b = color.z();
+ auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &p0, const QVector2D &p1, const QVector2D &p2){
- vertexBuffer.append({p.x(), p.y(), uv.x(), uv.y(), uv.z(), r, g, b, dbg});
- switch (wfIndex % 3) {
- case 0:
- wfVertices.append({p.x(), p.y(), 1.0f, 0.0f, 0.0f});
- break;
- case 1:
- wfVertices.append({p.x(), p.y(), 0.0f, 1.0f, 0.0f});
- break;
- case 2:
- wfVertices.append({p.x(), p.y(), 0.0f, 0.0f, 1.0f});
- break;
+ if (element.isLine()) {
+ node->appendTriangle(p0, p1, p2,
+ element.startPoint(), element.endPoint());
+ } else {
+ node->appendTriangle(p0, p1, p2,
+ element.startPoint(), element.controlPoint(), element.endPoint());
}
- };
-
- auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp, const QVector3D &color){
- int currentVertex = vertexBuffer.count();
- addVertexDataForPoint(element, sp, color, 0);
- addVertexDataForPoint(element, cp, color, 1);
- addVertexDataForPoint(element, ep, color, 2);
- indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ wfVertices.append({p0.x(), p0.y(), 1.0f, 0.0f, 0.0f});
+ wfVertices.append({p1.x(), p1.y(), 0.0f, 1.0f, 0.0f});
+ wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f});
};
// TESTING
- auto addBevelTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
+ auto addBevelTriangle = [&](const QVector2D &p0, const QVector2D &p1, const QVector2D &p2)
{
- float r = 0.0f;
- float g = 1.0f;
- float b = 0.5f;
-
-
- // MEGA HACK!!! Invent a curve based on the triangle, since we know that:
- // p2 is the path join point
- // the stroke passes through the mid points of p2-p1 and p2-p3
-
- // we have static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p)
- // which finds uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1
-
- QVector2D fp1 = p2 + (p3 - p1);
- QVector2D fp2 = p2 - (p3 - p1);
- QVector2D fcp = (p1 + 3 * p2 + p3) / 5; // does not matter for line
-
- // The bevel is not strokeWidth/2 wide, it's half this triangle's height
- // We need to shift everything down by the difference (definitely hackish)
+ QVector2D fp1 = p1 + (p0 - p1) / 2;
+ QVector2D fp2 = p1 + (p2 - p1) / 2;
+ QVector2D fcp = p1; // does not matter for line
- QVector2D vh = ((p1 - p2) + (p3 - p2)) / 2;
- float triangleHeight = vh.length();
- float delta = (pathData.pen.widthF() - triangleHeight) / 2;
+ // That describes a path that passes through those points. We want the stroke
+ // edge, so we need to shift everything down by the stroke offset
- QVector2D offset = vh.normalized() * delta;
- fp1 -= offset;
- fp2 -= offset;
- fcp -= offset;
+ QVector2D nn = calcNormalVector(p0, p2);
+ if (determinant(p0, p1, p2) < 0)
+ nn = -nn;
+ float delta = penWidth / 2;
+ QVector2D offset = nn.normalized() * delta;
+ fp1 += offset;
+ fp2 += offset;
+ fcp += offset;
- const auto uv1 = curveUv(fp1, fcp, fp2, p1);
- const auto uv2 = curveUv(fp1, fcp, fp2, p2);
- const auto uv3 = curveUv(fp1, fcp, fp2, p3);
+ node->appendTriangle(p0, p1, p2, fp1, fp2);
- const int currentVertex = vertexBuffer.count();
- vertexBuffer.append({p1.x(), p1.y(), uv1.x(), uv1.y(), 0.0f, r, g, b, dbg});
- vertexBuffer.append({p2.x(), p2.y(), uv2.x(), uv2.y(), 0.0f, r, g, b, dbg});
- vertexBuffer.append({p3.x(), p3.y(), uv3.x(), uv3.y(), 0.0f, r, g, b, dbg});
- indices << currentVertex << currentVertex + 1 << currentVertex + 2;
- wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f});
- wfVertices.append({p2.x(), p2.y(), 0.0f, 1.0f, 0.0f});
- wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f});
+ wfVertices.append({p0.x(), p0.y(), 1.0f, 0.0f, 0.0f});
+ wfVertices.append({p1.x(), p1.y(), 0.0f, 1.0f, 0.0f});
+ wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f});
//qDebug() << "bev" << p1 << p2 << p3;
};
- static constexpr QVector3D specialDebug{0.0f, 1.0f, 1.0f};
-
for (const auto &triangle : triangles) {
if (triangle.pathElementIndex < 0) {
- //TESTING
+ // TODO: improve bevel logic
addBevelTriangle(triangle.points[0], triangle.points[1], triangle.points[2]);
continue;
}
const auto &element = thePath.elementAt(triangle.pathElementIndex);
- addCurveTriangle(element, triangle.points[0], triangle.points[1], triangle.points[2],
- triangle.specialDebug ? specialDebug : triangle.debugColor);
+ addCurveTriangle(element, triangle.points[0], triangle.points[1], triangle.points[2]);
}
+ auto indexCopy = node->uncookedIndexes(); // uncookedIndexes get delete on cooking
+ node->setColor(color);
+ node->setStrokeWidth(pathData.pen.widthF());
+ node->cookGeometry();
+ m_rootNode->appendChildNode(node);
+ ret.append(node);
- if (indices.size() > 0) {
- auto *node = new QQuickShapeLoopBlinnNode(NoGradient); // TODO: dedicated node
- node->color = Qt::transparent;
- //node->fillGradient = pathData.gradient;
-
- node->strokeColor = pathData.pen.color(); // this is all you need to use the stroke shader
- node->strokeWidth = pathData.pen.widthF();
-
- QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
- vertexBuffer.size(), indices.size(), QSGGeometry::UnsignedIntType);
- node->setGeometry(g);
- g->setDrawingMode(QSGGeometry::DrawTriangles);
-
- //qDebug() << "gvc" << g->vertexCount();
-
- memcpy(g->vertexData(),
- vertexBuffer.data(),
- g->vertexCount() * g->sizeOfVertex());
- memcpy(g->indexData(),
- indices.data(),
- indices.size() * g->sizeOfIndex());
- m_rootNode->appendChildNode(node);
- ret.append(node);
- }
const bool wireFrame = debugVisualization() & DebugWireframe;
if (wireFrame) {
QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
- //QVarLengthArray<quint32> indices;
-
QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
wfVertices.size(),
- indices.size(),
+ indexCopy.size(),
QSGGeometry::UnsignedIntType);
wfNode->setGeometry(wfg);
wfg->setDrawingMode(QSGGeometry::DrawTriangles);
memcpy(wfg->indexData(),
- indices.data(),
- indices.size() * wfg->sizeOfIndex());
+ indexCopy.data(),
+ indexCopy.size() * wfg->sizeOfIndex());
memcpy(wfg->vertexData(),
wfVertices.data(),
wfg->vertexCount() * wfg->sizeOfVertex());
@@ -2589,6 +2033,8 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addNodesStrokeShader(const
m_rootNode->appendChildNode(wfNode);
debugNodes->append(wfNode);
}
+
+
return ret;
}
diff --git a/src/quickshapes/qquickshapecurverenderer_p.h b/src/quickshapes/qquickshapecurverenderer_p.h
index 859ff000c8..1832d53ec0 100644
--- a/src/quickshapes/qquickshapecurverenderer_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p.h
@@ -54,12 +54,14 @@ public:
void reserve(qsizetype size) { m_elements.reserve(size); }
qsizetype elementCount() const { return m_elements.size(); }
+ bool isEmpty() const { return m_elements.size() == 0; }
qsizetype elementCountRecursive() const;
static QuadPath fromPainterPath(const QPainterPath &path);
QPainterPath toPainterPath() const;
QuadPath subPathsClosed() const;
QuadPath flattened() const;
+ QuadPath dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset = 0) const;
class Element
{
@@ -111,6 +113,10 @@ public:
QVector3D uvForPoint(QVector2D p) const;
+ std::array<QVector2D, 3> ABC() const;
+
+ QVector3D HGForPoint(QVector2D p) const;
+
qsizetype childCount() const { return m_numChildren; }
qsizetype indexOfChild(qsizetype childNumber) const
@@ -134,6 +140,15 @@ public:
float extent() const;
+ void setAsConvex(bool isConvex)
+ {
+ if (isConvex)
+ m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags | Element::Convex);
+ else
+ m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::Convex);
+
+ }
+
private:
int intersectionsAtY(float y, float *fractions) const;
@@ -242,12 +257,6 @@ private:
QDebug operator<<(QDebug, const QuadPath::Element &);
QDebug operator<<(QDebug, const QuadPath &);
-class QQuickShapeCurveNode : public QSGNode
-{
-public:
- QQuickShapeCurveNode();
-};
-
class QQuickShapeCurveRenderer : public QQuickAbstractPathRenderer
{
public:
@@ -273,14 +282,16 @@ public:
void updateNode() override;
- void setRootNode(QQuickShapeCurveNode *node);
+ void setRootNode(QSGNode *node);
using NodeList = QVector<QSGGeometryNode *>;
enum DirtyFlag
{
- GeometryDirty = 1,
- UniformsDirty = 2
+ PathDirty = 0x01,
+ FillDirty = 0x02,
+ StrokeDirty = 0x04,
+ UniformsDirty = 0x08
};
enum DebugVisualizationOption {
@@ -302,14 +313,12 @@ private:
return validPenWidth && pen.color().alpha() > 0 && pen.style() != Qt::NoPen;
}
- bool useFragmentShaderStroker() const;
-
FillGradientType gradientType = NoGradient;
GradientDesc gradient;
- QPainterPath fillPath;
QPainterPath originalPath;
QuadPath path;
- QuadPath qPath; // TODO: better name
+ QuadPath fillPath;
+ QuadPath strokePath;
QColor fillColor;
Qt::FillRule fillRule = Qt::OddEvenFill;
QPen pen;
@@ -318,26 +327,20 @@ private:
bool convexConcaveResolved = false;
NodeList fillNodes;
+ NodeList fillDebugNodes;
NodeList strokeNodes;
- NodeList debugNodes;
+ NodeList strokeDebugNodes;
};
void deleteAndClear(NodeList *nodeList);
- QVector<QSGGeometryNode *> addPathNodesBasic(const PathData &pathData, NodeList *debugNodes);
- QVector<QSGGeometryNode *> addPathNodesLineShader(const PathData &pathData, NodeList *debugNodes);
- QVector<QSGGeometryNode *> addStrokeNodes(const PathData &pathData, NodeList *debugNodes);
- QVector<QSGGeometryNode *> addNodesStrokeShader(const PathData &pathData, NodeList *debugNodes);
-
- QSGGeometryNode *addLoopBlinnNodes(const QTriangleSet &triangles,
- const QVarLengthArray<quint32> &extraIndices,
- int startConcaveCurves,
- const PathData &pathData,
- NodeList *debugNodes);
+ QVector<QSGGeometryNode *> addFillNodes(const PathData &pathData, NodeList *debugNodes);
+ QVector<QSGGeometryNode *> addTriangulatingStrokerNodes(const PathData &pathData, NodeList *debugNodes);
+ QVector<QSGGeometryNode *> addCurveStrokeNodes(const PathData &pathData, NodeList *debugNodes);
void solveOverlaps(QuadPath &path);
- QQuickShapeCurveNode *m_rootNode;
+ QSGNode *m_rootNode;
QVector<PathData> m_paths;
static int debugVisualizationFlags;
};
diff --git a/src/quickshapes/qquickshapestrokenode.cpp b/src/quickshapes/qquickshapestrokenode.cpp
new file mode 100644
index 0000000000..624ddd094c
--- /dev/null
+++ b/src/quickshapes/qquickshapestrokenode.cpp
@@ -0,0 +1,140 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquickshapestrokenode_p.h"
+#include "qquickshapestrokenode_p_p.h"
+
+QT_BEGIN_NAMESPACE
+
+QQuickShapeStrokeNode::QQuickShapeStrokeNode()
+{
+ setFlag(OwnsGeometry, true);
+ setGeometry(new QSGGeometry(attributes(), 0, 0));
+ updateMaterial();
+}
+
+void QQuickShapeStrokeNode::QQuickShapeStrokeNode::updateMaterial()
+{
+ m_material.reset(new QQuickShapeStrokeMaterial(this));
+ setMaterial(m_material.data());
+}
+
+// Find the parameters H, G for the depressed cubic
+// t^2+H*t+G=0
+// that results from the equation
+// Q'(s).(p-Q(s)) = 0
+// The last parameter is the static offset between s and t:
+// s = t - b/(3a)
+// use it to get back the parameter t
+QVector3D QQuickShapeStrokeNode::HGforPoint(QVector2D q_a, QVector2D q_b, QVector2D q_c, QVector2D p)
+{
+ // this is a constant for the curve
+ float a = -2. * QVector2D::dotProduct(q_a, q_a);
+ // this is a constant for the curve
+ float b = -3. * QVector2D::dotProduct(q_a, q_b);
+ //this is linear in p so it can be put into the shader with vertex data
+ float c = 2. * QVector2D::dotProduct(q_a, p) - QVector2D::dotProduct(q_b, q_b) - 2. * QVector2D::dotProduct(q_a, q_c);
+ //this is linear in p so it can be put into the shader with vertex data
+ float d = QVector2D::dotProduct(q_b,p) - QVector2D::dotProduct(q_b, q_c);
+ // convert to depressed cubic.
+ // both functions are linear in c and d and thus linear in p
+ // Put in vertex data.
+ float H = (3. * a * c - b * b) / (3. * a * a);
+ float G = (2. * b * b * b - 9. * a * b * c + 27. * a * a * d) / (27. * a * a * a);
+
+ return QVector3D(H, G, b/(3*a));
+}
+
+// Take the start, control and end point of a curve and return the points A, B, C
+// representing the curve as Q(s) = A*s*s + B*s + C
+std::array<QVector2D, 3> QQuickShapeStrokeNode::curveABC(QVector2D p0, QVector2D p1, QVector2D p2)
+{
+ QVector2D a = p0 - 2*p1 + p2;
+ QVector2D b = 2*p1 - 2*p0;
+ QVector2D c = p0;
+
+ return {a, b, c};
+}
+
+void QQuickShapeStrokeNode::appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2,
+ const QVector2D &p0, const QVector2D &p1, const QVector2D &p2)
+{
+ auto abc = curveABC(p0, p1, p2);
+
+ int currentVertex = m_uncookedVertexes.count();
+
+ for (auto p : QList<QVector2D>({v0, v1, v2})) {
+ auto hg = HGforPoint(abc[0], abc[1], abc[2], p);
+
+ m_uncookedVertexes.append( { p.x(), p.y(),
+ abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(),
+ hg.x(), hg.y(),
+ hg.z()} );
+ }
+ m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2;
+}
+
+void QQuickShapeStrokeNode::appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2,
+ const QVector2D &p0, const QVector2D &p1)
+{
+ // We could reduce this to a linear equation by setting A to (0,0).
+ // However, then we cannot use the cubic solution and need an additional
+ // code path in the shader. The following formulation looks more complicated
+ // but allows to always use the cubic solution.
+ auto A = p1 - p0;
+ auto B = QVector2D(0., 0.);
+ auto C = p0;
+
+ int currentVertex = m_uncookedVertexes.count();
+
+ for (auto p : QList<QVector2D>({v0, v1, v2})) {
+ auto hg = HGforPoint(A, B, C, p);
+ m_uncookedVertexes.append( { p.x(), p.y(),
+ A.x(), A.y(), B.x(), B.y(), C.x(), C.y(),
+ hg.x(), hg.y(),
+ hg.z()} );
+ }
+ m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2;
+}
+
+void QQuickShapeStrokeNode::cookGeometry()
+{
+ QSGGeometry *g = geometry();
+ if (g->indexType() != QSGGeometry::UnsignedIntType) {
+ g = new QSGGeometry(attributes(),
+ m_uncookedVertexes.size(),
+ m_uncookedIndexes.size(),
+ QSGGeometry::UnsignedIntType);
+ setGeometry(g);
+ } else {
+ g->allocate(m_uncookedVertexes.size(), m_uncookedIndexes.size());
+ }
+
+ g->setDrawingMode(QSGGeometry::DrawTriangles);
+ memcpy(g->vertexData(),
+ m_uncookedVertexes.constData(),
+ g->vertexCount() * g->sizeOfVertex());
+ memcpy(g->indexData(),
+ m_uncookedIndexes.constData(),
+ g->indexCount() * g->sizeOfIndex());
+
+ m_uncookedIndexes.clear();
+ m_uncookedVertexes.clear();
+}
+
+const QSGGeometry::AttributeSet &QQuickShapeStrokeNode::attributes()
+{
+ static QSGGeometry::Attribute data[] = {
+ QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), //vertexCoord
+ QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //A
+ QSGGeometry::Attribute::createWithAttributeType(2, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //B
+ QSGGeometry::Attribute::createWithAttributeType(3, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //C
+ QSGGeometry::Attribute::createWithAttributeType(4, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //HG
+ QSGGeometry::Attribute::createWithAttributeType(5, 1, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //offset
+
+ };
+ static QSGGeometry::AttributeSet attrs = { 6, sizeof(StrokeVertex), data };
+ return attrs;
+}
+
+QT_END_NAMESPACE
diff --git a/src/quickshapes/qquickshapestrokenode_p.cpp b/src/quickshapes/qquickshapestrokenode_p.cpp
new file mode 100644
index 0000000000..5dae78a78d
--- /dev/null
+++ b/src/quickshapes/qquickshapestrokenode_p.cpp
@@ -0,0 +1,83 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquickshapestrokenode_p_p.h"
+#include "qquickshapestrokenode_p.h"
+
+#include "qquickshapegenericrenderer_p.h"
+
+QT_BEGIN_NAMESPACE
+
+bool QQuickShapeStrokeMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect)
+{
+ bool changed = false;
+ QByteArray *buf = state.uniformData();
+ Q_ASSERT(buf->size() >= 64);
+
+ if (state.isMatrixDirty()) {
+ const QMatrix4x4 m = state.combinedMatrix();
+ memcpy(buf->data(), m.constData(), 64);
+
+ float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio();
+ memcpy(buf->data()+64, &matrixScale, 4);
+ changed = true;
+ }
+
+ if (state.isOpacityDirty()) {
+ const float opacity = state.opacity();
+ memcpy(buf->data() + 64 + 4, &opacity, 4);
+ changed = true;
+ }
+
+ int offset = 64+16;
+
+ auto *newMaterial = static_cast<QQuickShapeStrokeMaterial *>(newEffect);
+ auto *oldMaterial = static_cast<QQuickShapeStrokeMaterial *>(oldEffect);
+
+ auto *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr;
+ auto *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr;
+
+ if (newNode == nullptr)
+ return changed;
+
+ QVector4D newStrokeColor(newNode->color().redF(),
+ newNode->color().greenF(),
+ newNode->color().blueF(),
+ newNode->color().alphaF());
+ QVector4D oldStrokeColor = oldNode != nullptr
+ ? QVector4D(oldNode->color().redF(),
+ oldNode->color().greenF(),
+ oldNode->color().blueF(),
+ oldNode->color().alphaF())
+ : QVector4D{};
+
+ if (oldNode == nullptr || oldStrokeColor != newStrokeColor) {
+ memcpy(buf->data() + offset, &newStrokeColor, 16);
+ changed = true;
+ }
+ offset += 16;
+
+ if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) {
+ float w = newNode->strokeWidth();
+ memcpy(buf->data() + offset, &w, 4);
+ changed = true;
+ }
+ //offset += 16;
+
+ return changed;
+}
+
+int QQuickShapeStrokeMaterial::compare(const QSGMaterial *other) const
+{
+ int typeDif = type() - other->type();
+ if (!typeDif) {
+ auto *othernode = static_cast<const QQuickShapeStrokeMaterial*>(other)->node();
+ if (node()->color() != othernode->color())
+ return node()->color().rgb() < othernode->color().rgb() ? -1 : 1;
+ if (node()->strokeWidth() != othernode->strokeWidth())
+ return node()->strokeWidth() < othernode->strokeWidth() ? -1 : 1;
+ }
+ return typeDif;
+}
+
+QT_END_NAMESPACE
diff --git a/src/quickshapes/qquickshapestrokenode_p.h b/src/quickshapes/qquickshapestrokenode_p.h
new file mode 100644
index 0000000000..e7ed4d17bb
--- /dev/null
+++ b/src/quickshapes/qquickshapestrokenode_p.h
@@ -0,0 +1,94 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKSHAPESTROKENODE_P_H
+#define QQUICKSHAPESTROKENODE_P_H
+
+#include <QtQuick/qsgnode.h>
+
+#include "qquickshapegenericrenderer_p.h"
+#include "qquickshapestrokenode_p_p.h"
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of a number of Qt sources files. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+QT_BEGIN_NAMESPACE
+
+class QQuickShapeStrokeNode : public QSGGeometryNode
+{
+public:
+ QQuickShapeStrokeNode();
+
+ void setColor(QColor col)
+ {
+ m_color = col;
+ }
+
+ QColor color() const
+ {
+ return m_color;
+ }
+
+ void setStrokeWidth(float width)
+ {
+ m_strokeWidth = width;
+ }
+
+ float strokeWidth() const
+ {
+ return m_strokeWidth;
+ }
+
+ void appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2, // triangle vertices
+ const QVector2D &p0, const QVector2D &p1, const QVector2D &p2); // curve points
+
+ void appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2, // triangle vertices
+ const QVector2D &p0, const QVector2D &p1); // line points
+
+ void cookGeometry();
+
+ static const QSGGeometry::AttributeSet &attributes();
+
+ QVector<quint32> uncookedIndexes() const
+ {
+ return m_uncookedIndexes;
+ }
+
+private:
+
+ struct StrokeVertex
+ {
+ float x, y;
+ float ax, ay;
+ float bx, by;
+ float cx, cy;
+ float H, G; //depressed cubic parameters
+ float offset; //mapping between depressed and happy cubic
+ };
+
+ void updateMaterial();
+
+ static QVector3D HGforPoint(QVector2D A, QVector2D B, QVector2D C, QVector2D p);
+ static std::array<QVector2D, 3> curveABC(QVector2D p0, QVector2D p1, QVector2D p2);
+
+ QColor m_color;
+ float m_strokeWidth = 0.0f;
+
+protected:
+ QScopedPointer<QQuickShapeStrokeMaterial> m_material;
+
+ QVector<StrokeVertex> m_uncookedVertexes;
+ QVector<quint32> m_uncookedIndexes;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKSHAPESTROKENODE_P_H
diff --git a/src/quickshapes/qquickshapestrokenode_p_p.h b/src/quickshapes/qquickshapestrokenode_p_p.h
new file mode 100644
index 0000000000..97fcad3cd8
--- /dev/null
+++ b/src/quickshapes/qquickshapestrokenode_p_p.h
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKSHAPESTROKENODE_P_P_H
+#define QQUICKSHAPESTROKENODE_P_P_H
+
+#include <QtQuick/qsgmaterial.h>
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of a number of Qt sources files. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+QT_BEGIN_NAMESPACE
+
+class QQuickShapeStrokeNode;
+class QQuickShapeStrokeMaterial;
+
+class QQuickShapeStrokeMaterialShader : public QSGMaterialShader
+{
+public:
+ QQuickShapeStrokeMaterialShader()
+ {
+ setShaderFileName(VertexStage,
+ QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapestroke.vert.qsb"));
+ setShaderFileName(FragmentStage,
+ QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapestroke.frag.qsb"));
+ }
+
+ bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override;
+};
+
+
+class QQuickShapeStrokeMaterial : public QSGMaterial
+{
+public:
+ QQuickShapeStrokeMaterial(QQuickShapeStrokeNode *node)
+ : m_node(node)
+ {
+ setFlag(Blending, true);
+ }
+
+ int compare(const QSGMaterial *other) const override;
+
+ QQuickShapeStrokeNode *node() const
+ {
+ return m_node;
+ }
+
+protected:
+ QSGMaterialType *type() const override
+ {
+ static QSGMaterialType t;
+ return &t;
+ }
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override
+ {
+ return new QQuickShapeStrokeMaterialShader;
+ }
+
+ QQuickShapeStrokeNode *m_node;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKSHAPESTROKENODE_P_P_H
diff --git a/src/quickshapes/shaders_ng/shapecurve.frag b/src/quickshapes/shaders_ng/shapecurve.frag
index c9eb946c2d..da614a9de3 100644
--- a/src/quickshapes/shaders_ng/shapecurve.frag
+++ b/src/quickshapes/shaders_ng/shapecurve.frag
@@ -1,25 +1,35 @@
#version 440
-layout(location = 0) in vec3 qt_TexCoord;
+layout(location = 0) in vec4 qt_TexCoord;
layout(location = 1) in vec4 debugColor;
#if defined(LINEARGRADIENT)
layout(location = 2) in float gradTabIndex;
+# define NEXT_LOCATION 3
#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT)
layout(location = 2) in vec2 coord;
+# define NEXT_LOCATION 3
+#else
+# define NEXT_LOCATION 2
#endif
+layout(location = NEXT_LOCATION) in vec4 gradient;
+
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
+ float matrixScale;
+ float opacity;
+ float reserved2;
+ float reserved3;
#if defined(STROKE)
vec4 strokeColor;
float strokeWidth;
- float reserved1;
- float reserved2;
- float reserved3;
+ float reserved4;
+ float reserved5;
+ float reserved6;
#endif
#if defined(LINEARGRADIENT)
@@ -80,10 +90,50 @@ void main()
float f = qt_TexCoord.z * (qt_TexCoord.x * qt_TexCoord.x - qt_TexCoord.y) // curve
+ (1.0 - abs(qt_TexCoord.z)) * (qt_TexCoord.x - qt_TexCoord.y); // line
-#if defined(STROKE)
+#if defined(USE_DERIVATIVES)
float _ddx = dFdx(f);
float _ddy = dFdy(f);
float df = length(vec2(_ddx, _ddy));
+#else
+ // We calculate the partial derivatives for f'(x, y) based on knowing the partial derivatives
+ // for the texture coordinates (u, v).
+ // So for curves:
+ // f(x,y) = u(x, y) * u(x, y) - v(x, y)
+ // f(x,y) = p(u(x,y)) - v(x, y) where p(u) = u^2
+ // So f'(x, y) = p'(u(x, y)) * u'(x, y) - v'(x, y)
+ // (by chain rule and sum rule)
+ // f'(x, y) = 2 * u(x, y) * u'(x, y) - v'(x, y)
+ // And so:
+ // df/dx = 2 * u(x, y) * du/dx - dv/dx
+ // df/dy = 2 * u(x, y) * du/dy - dv/dy
+ //
+ // and similarly for straight lines:
+ // f(x, y) = u(x, y) - v(x, y)
+ // f'(x, y) = dudx - dvdx
+
+ float dudx = gradient.x;
+ float dvdx = gradient.y;
+ float dudy = gradient.z;
+ float dvdy = gradient.w;
+
+ // Test with analytic derivatives
+// dudx = dFdx(qt_TexCoord.x);
+// dvdx = dFdx(qt_TexCoord.y);
+// dudy = dFdy(qt_TexCoord.x);
+// dvdy = dFdy(qt_TexCoord.y);
+
+ float dfx_curve = 2.0f * qt_TexCoord.x * dudx - dvdx;
+ float dfy_curve = 2.0f * qt_TexCoord.x * dudy - dvdy;
+
+ float dfx_line = dudx - dvdx;
+ float dfy_line = dudy - dvdy;
+
+ float dfx = qt_TexCoord.z * dfx_curve + (1.0 - abs(qt_TexCoord.z)) * dfx_line;
+ float dfy = qt_TexCoord.z * dfy_curve + (1.0 - abs(qt_TexCoord.z)) * dfy_line;
+ float df = length(vec2(dfx, dfy));
+#endif
+
+#if defined(STROKE)
float distance = (f / df); // distance from centre of fragment to line
float halfStrokeWidth = ubuf.strokeWidth / 2.0;
@@ -98,11 +148,8 @@ void main()
vec4 combined = fill * (1.0 - stroke.a) + stroke * stroke.a;
// finally mix in debug
- fragColor = mix(combined, vec4(debugColor.rgb, 1.0), debugColor.a);
-
- // TODO: support both outline and fill
+ fragColor = mix(combined, vec4(debugColor.rgb, 1.0), debugColor.a) * ubuf.opacity;
#else
- float df = fwidth(f);
- fragColor = mix(baseColor() * clamp(0.5 + f / df, 0.0, 1.0), vec4(debugColor.rgb, 1.0), debugColor.a);
+ fragColor = mix(baseColor() * clamp(0.5 + f / df, 0.0, 1.0), vec4(debugColor.rgb, 1.0), debugColor.a) * ubuf.opacity;
#endif
}
diff --git a/src/quickshapes/shaders_ng/shapecurve.vert b/src/quickshapes/shaders_ng/shapecurve.vert
index 3bd281ddaa..c20a22f613 100644
--- a/src/quickshapes/shaders_ng/shapecurve.vert
+++ b/src/quickshapes/shaders_ng/shapecurve.vert
@@ -1,27 +1,38 @@
#version 440
layout(location = 0) in vec4 vertexCoord;
-layout(location = 1) in vec3 vertexTexCoord;
+layout(location = 1) in vec4 vertexTexCoord;
layout(location = 2) in vec4 vertexDebugColor;
+layout(location = 3) in vec4 vertexGradient;
-layout(location = 0) out vec3 qt_TexCoord;
+layout(location = 0) out vec4 qt_TexCoord;
layout(location = 1) out vec4 debugColor;
#if defined(LINEARGRADIENT)
layout(location = 2) out float gradTabIndex;
+# define NEXT_LOCATION 3
#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT)
layout(location = 2) out vec2 coord;
+# define NEXT_LOCATION 3
+#else
+# define NEXT_LOCATION 2
#endif
+layout(location = NEXT_LOCATION) out vec4 gradient;
+
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
+ float matrixScale;
+ float opacity;
+ float reserved2;
+ float reserved3;
#if defined(STROKE)
vec4 strokeColor;
float strokeWidth;
- float reserved1;
- float reserved2;
- float reserved3;
+ float reserved4;
+ float reserved5;
+ float reserved6;
#endif
#if defined(LINEARGRADIENT)
@@ -46,6 +57,7 @@ void main()
{
qt_TexCoord = vertexTexCoord;
debugColor = vertexDebugColor;
+ gradient = vertexGradient / ubuf.matrixScale;
#if defined(LINEARGRADIENT)
vec2 gradVec = ubuf.gradientEnd - ubuf.gradientStart;
diff --git a/src/quickshapes/shaders_ng/shapestroke.frag b/src/quickshapes/shaders_ng/shapestroke.frag
new file mode 100644
index 0000000000..b7f4a09f89
--- /dev/null
+++ b/src/quickshapes/shaders_ng/shapestroke.frag
@@ -0,0 +1,129 @@
+#version 440
+
+layout(location = 0) in vec4 P;
+layout(location = 1) in vec2 A;
+layout(location = 2) in vec2 B;
+layout(location = 3) in vec2 C;
+layout(location = 4) in vec2 HG;
+layout(location = 5) in float offset;
+
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+
+ float matrixScale;
+ float opacity;
+ float reserved2;
+ float reserved3;
+
+ vec4 strokeColor;
+
+ float strokeWidth;
+ float reserved4;
+ float reserved5;
+ float reserved6;
+} ubuf;
+
+float cuberoot(float x)
+{
+ return sign(x) * pow(abs(x), 1 / 3.);
+}
+
+#define PI 3.1415926538
+
+vec3 solveDepressedCubic(float p, float q)
+{
+ float D = q * q / 4. + p * p * p / 27.;
+
+ float u1 = cuberoot(-q / 2. - sqrt(D));
+ float u2 = cuberoot(-q / 2. + sqrt(D));
+ vec3 rootsD1 = vec3(u1 - p / (3. * u1), u2 - p / (3. * u2), 0);
+
+ float v = 2.*sqrt(-p / 3.);
+ float t = acos(3. * q / p / v) / 3.;
+ float k = 2. * PI / 3.;
+ vec3 rootsD2 = vec3(v * cos(t), v * cos(t - k), v * cos(t - 2. * k));
+
+ return D > 0 ? rootsD1 : rootsD2;
+}
+
+mat2 qInverse(mat2 matrix) {
+ float a = matrix[0][0], b = matrix[0][1];
+ float c = matrix[1][0], d = matrix[1][1];
+
+ float determinant = a * d - b * c;
+ float invDet = 1.0 / determinant;
+
+ mat2 inverseMatrix;
+ inverseMatrix[0][0] = d * invDet;
+ inverseMatrix[0][1] = -b * invDet;
+ inverseMatrix[1][0] = -c * invDet;
+ inverseMatrix[1][1] = a * invDet;
+
+ return inverseMatrix;
+}
+
+void main()
+{
+ vec3 s = solveDepressedCubic(HG.x, HG.y) - vec3(offset, offset, offset);
+
+ vec2 Qmin = vec2(1e10, 1e10);
+ float dmin = 1e4;
+ for (int i = 0; i < 3; i++) {
+ float t = clamp(s[i], 0., 1.);
+ vec2 Q = A * t * t + B * t + C;
+ float d = length(Q - P.xy);
+ float foundNewMin = step(d, dmin);
+ dmin = min(d, dmin);
+ Qmin = foundNewMin * Q + (1. - foundNewMin) * Qmin;
+ }
+ vec2 n = (P.xy - Qmin) / dmin;
+ vec2 Q1 = (Qmin + ubuf.strokeWidth / 2. * n);
+ vec2 Q2 = (Qmin - ubuf.strokeWidth / 2. * n);
+
+ // Converting to screen coordinates:
+#if defined(USE_DERIVATIVES)
+ mat2 T = mat2(dFdx(P.x), dFdy(P.x), dFdx(P.y), dFdy(P.y));
+ mat2 Tinv = qInverse(T);
+ vec2 Q1_s = Tinv * Q1;
+ vec2 Q2_s = Tinv * Q2;
+ vec2 P_s = Tinv * P.xy;
+ vec2 n_s = Tinv * n;
+ n_s = n_s / length(n_s);
+#else
+ vec2 Q1_s = ubuf.matrixScale * Q1;
+ vec2 Q2_s = ubuf.matrixScale * Q2;
+ vec2 P_s = ubuf.matrixScale * P.xy;
+ vec2 n_s = n;
+#endif
+
+ // Geometric solution for anti aliasing using the known distances
+ // to the edges of the path in the screen coordinate system.
+ float dist1 = dot(P_s - Q1_s, n_s);
+ float dist2 = dot(P_s - Q2_s, n_s);
+
+ // Calculate the fill coverage if the line is crossing the square cell
+ // normally (vertically or horizontally).
+ // float fillCoverageLin = clamp(0.5-dist1, 0., 1.) - clamp(0.5-dist2, 0., 1.);
+
+ // Calculate the fill coverage if the line is crossing the square cell
+ // diagonally.
+ float fillCoverageDia = clamp(step(0., -dist1) + sign(dist1) * pow(max(0., sqrt(2.) / 2. - abs(dist1)), 2.), 0., 1.) -
+ clamp(step(0., -dist2) + sign(dist2) * pow(max(0., sqrt(2.) / 2. - abs(dist2)), 2.), 0., 1.);
+
+ // Merge the normal and the diagonal solution. The merge factor is periodic
+ // in 90 degrees and 0/1 at 0 and 45 degree. The simple equation was
+ // estimated after numerical experiments.
+ // float mergeFactor = 2 * abs(n_s.x) * abs(n_s.y);
+ // float fillCoverage = mergeFactor * fillCoverageDia + (1-mergeFactor) * fillCoverageLin;
+
+ // It seems to be sufficient to use the equation for the diagonal.
+ float fillCoverage = fillCoverageDia;
+
+ // The center line is sometimes not filled because of numerical issues. This fixes this.
+ float centerline = step(ubuf.strokeWidth * 0.01, dmin);
+ fillCoverage = fillCoverage * centerline + min(1., ubuf.strokeWidth * ubuf.matrixScale) * (1. - centerline);
+
+ fragColor = vec4(ubuf.strokeColor.rgb, 1.0) *ubuf.strokeColor.a * fillCoverage * ubuf.opacity;
+}
diff --git a/src/quickshapes/shaders_ng/shapestroke.vert b/src/quickshapes/shaders_ng/shapestroke.vert
new file mode 100644
index 0000000000..90c67deb16
--- /dev/null
+++ b/src/quickshapes/shaders_ng/shapestroke.vert
@@ -0,0 +1,46 @@
+#version 440
+
+layout(location = 0) in vec4 vertexCoord;
+layout(location = 1) in vec2 inA;
+layout(location = 2) in vec2 inB;
+layout(location = 3) in vec2 inC;
+layout(location = 4) in vec2 inHG;
+layout(location = 5) in float inoffset;
+
+layout(location = 0) out vec4 P;
+layout(location = 1) out vec2 A;
+layout(location = 2) out vec2 B;
+layout(location = 3) out vec2 C;
+layout(location = 4) out vec2 HG;
+layout(location = 5) out float offset;
+
+
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+
+ float matrixScale;
+ float opacity;
+ float reserved2;
+ float reserved3;
+
+ vec4 strokeColor;
+
+ float strokeWidth;
+ float reserved4;
+ float reserved5;
+ float reserved6;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main()
+{
+ P = vertexCoord;
+ A = inA;
+ B = inB;
+ C = inC;
+ HG = inHG;
+ offset = inoffset;
+
+ gl_Position = ubuf.qt_Matrix * vertexCoord;
+}
diff --git a/tests/manual/painterpathquickshape/CMakeLists.txt b/tests/manual/painterpathquickshape/CMakeLists.txt
index 67f5b142ef..1f92afaba5 100644
--- a/tests/manual/painterpathquickshape/CMakeLists.txt
+++ b/tests/manual/painterpathquickshape/CMakeLists.txt
@@ -1,6 +1,15 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
+cmake_minimum_required(VERSION 3.16)
+
+if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ project(painterPathQuickShape LANGUAGES C CXX ASM)
+ find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST)
+endif()
+
+find_package(Qt6 COMPONENTS ShaderTools)
+
qt_internal_add_manual_test(painterPathQuickShape
GUI
SOURCES
@@ -20,35 +29,36 @@ qt_internal_add_manual_test(painterPathQuickShape
set(qml_resource_files
"1535737773.svg"
"main.qml"
- "SvgShape.qml"
- "ControlPanel.qml"
- "ControlPoint.qml"
- "TextShape.qml"
- "SimpleShape.qml"
- "SmallPolygon.qml"
- "Squircle.qml"
- "ControlledShape.qml"
- "Mussel.qml"
- "Graziano.ttf"
- "CubicShape.qml"
- "hand-print.svg"
- "background.png"
- "arcDirection.qml"
- "arcRotation.qml"
- "capStyles.qml"
- "cubicCurve.qml"
- "dashPattern.qml"
- "ellipticalArcs.qml"
- "fillRules.qml"
- "gradientSpreadModes.qml"
- "joinStyles.qml"
- "largeOrSmallArc.qml"
- "linearGradient.qml"
- "quadraticCurve.qml"
- "radialGradient.qml"
- "strokeOrFill.qml"
- "text.qml"
- "tiger.qml"
+ "SvgShape.qml"
+ "ControlPanel.qml"
+ "ControlPoint.qml"
+ "TextShape.qml"
+ "SimpleShape.qml"
+ "SmallPolygon.qml"
+ "Squircle.qml"
+ "ControlledShape.qml"
+ "Mussel.qml"
+ "Graziano.ttf"
+ "CubicShape.qml"
+ "DashedStroke.qml"
+ "hand-print.svg"
+ "background.png"
+ "arcDirection.qml"
+ "arcRotation.qml"
+ "capStyles.qml"
+ "cubicCurve.qml"
+ "dashPattern.qml"
+ "ellipticalArcs.qml"
+ "fillRules.qml"
+ "gradientSpreadModes.qml"
+ "joinStyles.qml"
+ "largeOrSmallArc.qml"
+ "linearGradient.qml"
+ "quadraticCurve.qml"
+ "radialGradient.qml"
+ "strokeOrFill.qml"
+ "text.qml"
+ "tiger.qml"
)
qt_internal_add_resource(painterPathQuickShape "qml"
@@ -58,6 +68,16 @@ qt_internal_add_resource(painterPathQuickShape "qml"
${qml_resource_files}
)
+qt_add_shaders(painterPathQuickShape "shaders"
+ BATCHABLE
+ PRECOMPILE
+ OPTIMIZED
+ PREFIX
+ "/"
+ FILES
+ "zoombox.frag"
+)
+
qt_add_qml_module(painterPathQuickShape
VERSION 1.0
URI InstancingTest
diff --git a/tests/manual/painterpathquickshape/ControlPanel.qml b/tests/manual/painterpathquickshape/ControlPanel.qml
index 1b34bb6eab..bb26e658f3 100644
--- a/tests/manual/painterpathquickshape/ControlPanel.qml
+++ b/tests/manual/painterpathquickshape/ControlPanel.qml
@@ -6,6 +6,7 @@ import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Shapes
import QtQuick.Dialogs
+import QtCore
Item {
property real scale: +scaleSlider.value.toFixed(4)
@@ -15,7 +16,7 @@ Item {
property color fillColor: Qt.rgba(fillColor.color.r, fillColor.color.g, fillColor.color.b, pathAlpha)
property alias pathAlpha: alphaSlider.value
property alias outlineAlpha: outlineAlphaSlider.value
- property real outlineWidth: cosmeticPen.checked ? outlineWidth.value / scale : outlineWidth.value ** 2
+ property real outlineWidth: cosmeticPen.checked ? outlineWidthEdit.text / scale : outlineWidthEdit.text
property alias outlineStyle: outlineStyle.currentValue
property alias capStyle: capStyle.currentValue
property alias joinStyle: joinStyle.currentValue
@@ -34,6 +35,23 @@ Item {
property real pathMargin: marginEdit.text
+ Settings {
+ property alias enableOutline: enableOutline.checked
+ property alias outlineColor: outlineColor.color
+ property alias outlineWidth: outlineWidthEdit.text
+ property alias outlineAlpha: outlineAlphaSlider.value
+ property alias outlineStyle: outlineStyle.currentIndex
+ property alias joinStyle: joinStyle.currentIndex
+ property alias capStyle: capStyle.currentIndex
+ property alias cosmeticPen: cosmeticPen.checked
+
+ property alias pathAlpha: alphaSlider.value
+ property alias fillColor: fillColor.color
+
+ property alias setBackground: setBackground.checked
+ property alias backgroundColor: bgColor.color
+ }
+
function setScale(x) {
scaleSlider.value = x
}
@@ -200,7 +218,7 @@ Item {
model: [ "NoGradient", "LinearGradient", "RadialGradient", "ConicalGradient" ]
}
Label {
- text: "Path alpha:"
+ text: "Fill alpha(" + Math.round(alphaSlider.value*100)/100 + "):"
color: "white"
}
Slider {
@@ -290,11 +308,24 @@ Item {
}
}
Label {
- text: "Outline width"
+ text: "Outline width:"
color: "white"
}
+ TextField {
+ id: outlineWidthEdit
+ text: (cosmeticPen.checked ? outlineWidthSlider.value: outlineWidthSlider.value ** 2).toFixed(2)
+ onEditingFinished: {
+ let val = +text
+ if (val > 0) {
+ if (cosmeticPen.checked)
+ outlineWidth.value = val * scale
+ else
+ outlineWidth.value = Math.sqrt(val)
+ }
+ }
+ }
Slider {
- id: outlineWidth
+ id: outlineWidthSlider
Layout.fillWidth: true
from: 0.0
to: 10.0
@@ -306,7 +337,7 @@ Item {
palette.windowText: "white"
}
Label {
- text: "Outline alpha"
+ text: "Outline alpha (" + Math.round(outlineAlphaSlider.value*100)/100 + "):"
color: "white"
}
Slider {
diff --git a/tests/manual/painterpathquickshape/ControlledShape.qml b/tests/manual/painterpathquickshape/ControlledShape.qml
index cf9cbaa5d8..b0f4492eb2 100644
--- a/tests/manual/painterpathquickshape/ControlledShape.qml
+++ b/tests/manual/painterpathquickshape/ControlledShape.qml
@@ -7,12 +7,14 @@ import io.qt
Item {
id: topLevel
- property alias fillColor: shapePath.fillColor
- property alias strokeStyle: shapePath.strokeStyle
property alias capStyle: shapePath.capStyle
+ property alias dashOffset: shapePath.dashOffset
+ property alias dashPattern: shapePath.dashPattern
+ property alias fillColor: shapePath.fillColor
+ property alias fillRule: shapePath.fillRule
property alias strokeColor: shapePath.strokeColor
+ property alias strokeStyle: shapePath.strokeStyle
property alias strokeWidth: shapePath.strokeWidth
- property alias fillRule: shapePath.fillRule
property alias shapeTransform: shape.transform
property alias startX: shapePath.startX
diff --git a/tests/manual/painterpathquickshape/DashedStroke.qml b/tests/manual/painterpathquickshape/DashedStroke.qml
new file mode 100644
index 0000000000..60952ec599
--- /dev/null
+++ b/tests/manual/painterpathquickshape/DashedStroke.qml
@@ -0,0 +1,24 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Shapes
+
+ControlledShape {
+ fillColor: "transparent"
+ strokeStyle: ShapePath.DashLine
+ dashOffset: 6.5
+ dashPattern: [ 4, 2, 1, 2, 1, 2 ]
+ delegate: [
+ PathMove { x: 200; y: 200 },
+ PathLine { x: 1000; y: 200 },
+ PathMove { x: 200; y: 300 },
+ PathLine { x: 1000; y: 300 },
+ PathMove { x: 200; y: 400 },
+ PathQuad { x: 1000; y: 400; controlX: 450; controlY: 600 },
+ PathLine { x: 1600; y: 500 },
+ PathMove { x: 200; y: 500 },
+ PathQuad { x: 1000; y: 500; controlX: 450; controlY: 700 },
+ PathLine { x: 1600; y: 600 }
+ ]
+}
diff --git a/tests/manual/painterpathquickshape/SimpleShape.qml b/tests/manual/painterpathquickshape/SimpleShape.qml
index 3458e26f4a..cb6261c39a 100644
--- a/tests/manual/painterpathquickshape/SimpleShape.qml
+++ b/tests/manual/painterpathquickshape/SimpleShape.qml
@@ -11,12 +11,11 @@ ControlledShape {
PathPolyline {
id: ppl
path: [ Qt.point(150.0, 100.0),
- Qt.point(1250.0, 150.0),
- Qt.point(100.0, 1000.0),
- Qt.point(150, 100)
- ]
+ Qt.point(1250.0, 150.0),
+ Qt.point(100.0, 1000.0),
+ Qt.point(150, 100)
+ ]
},
-
// A very narrow shape with one convex and one concave curve
PathMove { x: 600; y: 1200},
PathQuad { x: 800; y: 1200; controlX: 700; controlY: 600 },
diff --git a/tests/manual/painterpathquickshape/SvgShape.qml b/tests/manual/painterpathquickshape/SvgShape.qml
index 697ede2524..429ca507e6 100644
--- a/tests/manual/painterpathquickshape/SvgShape.qml
+++ b/tests/manual/painterpathquickshape/SvgShape.qml
@@ -63,12 +63,13 @@ Item {
strokeWidth = "1.0" // default value defined by SVG standard
strokeText = "strokeColor: \"" + strokeColor + "\"; strokeWidth: " + strokeWidth + ";"
}
- if (!fillColor) {
- fillColor = "#00000000"
- }
+
+ let fillText = "";
+ if (fillColor)
+ fillText = "fillColor: \"" + fillColor + "\";\n";
var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { \n"
- + "fillColor: \"" + fillColor + "\";\n"
+ + fillText
+ "shapeTransform: Matrix4x4 { matrix: Qt.matrix4x4(" + transform + "); }\n"
+ strokeText + "\n"
+ "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }\n",
diff --git a/tests/manual/painterpathquickshape/main.cpp b/tests/manual/painterpathquickshape/main.cpp
index 5b780b9439..c1fa790bd4 100644
--- a/tests/manual/painterpathquickshape/main.cpp
+++ b/tests/manual/painterpathquickshape/main.cpp
@@ -15,6 +15,7 @@ int main(int argc, char *argv[])
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
+ app.setOrganizationName("QtProject");
qmlRegisterType<DebugPaintItem>("io.qt", 1, 0, "DebugPaintItem");
qmlRegisterType<SvgPathLoader>("io.qt", 1, 0, "SvgPathLoader");
diff --git a/tests/manual/painterpathquickshape/main.qml b/tests/manual/painterpathquickshape/main.qml
index 9ad5d1f1a4..c7c1f60579 100644
--- a/tests/manual/painterpathquickshape/main.qml
+++ b/tests/manual/painterpathquickshape/main.qml
@@ -6,8 +6,10 @@ import QtQuick.Window
import QtQuick.Shapes
import QtQuick.Controls
import QtQuick.Layouts
+import QtCore
Window {
+ id: theWindow
width: 1024
height: 768
visible: true
@@ -42,6 +44,10 @@ Window {
source: "CubicShape.qml"
}
ListElement {
+ text: "DashedStroke"
+ source: "DashedStroke.qml"
+ }
+ ListElement {
text: "Mussel"
source: "Mussel.qml"
}
@@ -189,8 +195,80 @@ Window {
y: controlPanel.pathMargin
id: loader
}
+ MouseArea {
+ acceptedButtons: Qt.NoButton
+ anchors.fill: parent
+ onMouseXChanged: {
+ let p = Qt.point(Math.round(mouseX), Math.round(mouseY))
+ p = mapToItem(loader.item, p)
+ zoomTarget.sourceRect.x = p.x - zoomTarget.sourceRect.width/2
+ }
+ onMouseYChanged: {
+ let p = Qt.point(Math.round(mouseX), Math.round(mouseY))
+ p = mapToItem(loader.item, p)
+ zoomTarget.sourceRect.y = p.y - zoomTarget.sourceRect.height/2
+ }
+ hoverEnabled: true
+ }
+ ShaderEffectSource {
+ id: zoomTarget
+ sourceItem: loader.item
+ sourceRect.width: 16
+ sourceRect.height: 16
+ }
+ }
+
+
+ Rectangle {
+ anchors.top: flickable.top
+ anchors.right: flickable.right
+ anchors.margins: 5
+ width: 256
+ height: 256
+ border.color: Qt.black
+ ShaderEffect {
+ anchors.fill: parent
+ anchors.margins: 1
+ property variant src: zoomTarget
+ property real textureWidth: zoomTarget.sourceRect.width
+ property real textureHeight: zoomTarget.sourceRect.height
+ fragmentShader: "zoombox.frag.qsb"
+ }
+ Button {
+ id: plusButton
+ text: "+"
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: 2
+ width: 20
+ height: 20
+ onPressed: {
+ zoomTarget.sourceRect.width = Math.max(zoomTarget.sourceRect.width / 2, 4)
+ zoomTarget.sourceRect.height = Math.max(zoomTarget.sourceRect.height / 2, 4)
+ }
+ }
+ Button {
+ id: minusButton
+ text: "-"
+ anchors.top: plusButton.bottom
+ anchors.right: parent.right
+ anchors.margins: 2
+ width: 20
+ height: 20
+ onPressed: {
+ zoomTarget.sourceRect.width = Math.max(zoomTarget.sourceRect.width * 2, 4)
+ zoomTarget.sourceRect.height = Math.max(zoomTarget.sourceRect.height * 2, 4)
+ }
+ }
+ Text {
+ text: "x"+parent.width / zoomTarget.sourceRect.width
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.margins: 2
+ }
}
+
ControlPanel {
id: controlPanel
anchors.bottom: parent.bottom
@@ -198,4 +276,9 @@ Window {
anchors.right: parent.right
height: parent.height / 4
}
+ Settings {
+ property alias currentTab: comboBox.currentIndex
+ property alias windowWidth: theWindow.width
+ property alias windowHeight: theWindow.height
+ }
}
diff --git a/tests/manual/painterpathquickshape/zoombox.frag b/tests/manual/painterpathquickshape/zoombox.frag
new file mode 100644
index 0000000000..e7754cb75c
--- /dev/null
+++ b/tests/manual/painterpathquickshape/zoombox.frag
@@ -0,0 +1,22 @@
+#version 440
+
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+ float qt_Opacity;
+ float textureWidth;
+ float textureHeight;
+};
+layout(binding = 1) uniform sampler2D src;
+
+
+void main(void)
+{
+
+ float xPixelIndex = (round((textureWidth - 1.) * qt_TexCoord0.x) + 0.5) / textureWidth;
+ float yPixelIndex = (round((textureHeight - 1.) * qt_TexCoord0.y) + 0.5) / textureHeight;
+
+ fragColor = texture(src, vec2(xPixelIndex, yPixelIndex)) * qt_Opacity;
+}