aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <[email protected]>2024-12-04 22:10:09 +0100
committerShawn Rutledge <[email protected]>2025-09-12 23:25:21 +0200
commit3f89d9deb0bcdfdfe2b8534603adf235edc4a9f0 (patch)
tree978174f9608b49963af672228dff55e10942945c
parent6803e9c90862f650cb016dc5554efdcc66978e6b (diff)
Add ShapePath.cosmeticStroke
QSvgRenderer already obeys the QPen::isCosmetic() setting. Now we add a ShapePath.cosmeticStroke property and try to support cosmetic stroking both with SVG and with Shapes, in all renderers. The curve renderer now starts with skinny triangles: each segment's end vertices are passed to the vertex shader with their original positions, and the shader uses the normal and the stroke width to expand the stroke triangles outwards as needed. We always add triangles for end caps, regardless whether they are square or round. We get rid of addBevelTriangle and fix bevels, square caps and miters: All three of these cases are done by drawing lines adjusted to the right direction within the respective triangles. And to avoid seeing rounded ends at any reasonable zoom level, we need the line equation coefficients to take the line very far outside the actual triangles. Square caps are really square: we render line segments in those three triangles, not extensions of the adjacent curve or line. Miters are also rendered as straight tangent lines. The bevel's triangle is very short when the join is an acute angle, and almost as tall as the full stroke width when the join is very obtuse. But when the triangle is short, we need to diminish the stroke width rendered in the fragment shader so that the center of the stroke falls on the inner corner of that triangle, and the edge of the stroke is rendered along the outer bevel edge rather than trying to go outside. That's achieved by multiplying the stroke width by the cosine of half the total angle, AKA the dot product. That is now in the normalExt.z vertex attribute. Normals (normalExt.xy) can be premultiplied rather than normalized: in fact some of them already have length > 1. In qsgbatchrenderer, Renderer::prepareAlphaBatches() breaks batches when overlaps occur. Now that we stroke lines with vertices that represent them as zero-width lines (and thus Element.bounds has the same x or y coordinates on both corners), we must consider lines right on top of each other to be overlapping: e.g. the stacks of horizontal (dashed) line segments in paint-stroke-202-t.svg must be in separate batches. At the time QQuickShape::updatePaintNode() is called, the available transform node (from UpdatePaintNodeData or an individual node's parent which is a transform node) does not contain the scaling factor that we need to allow for the stroke width to be adjusted for cosmetic stroking. But in QQuickShapePrivate::sync(), windowToItemTransform() is known, and from bde55ad574ac84440e2cdc9c1122a344bb1cb67a we have a precedent in QSGCurveStrokeMaterialShader::updateUniformData() for using the square root of the matrix determinant as a scaling approximation (ok when the scaling is uniform). QQuickShapeSoftwareRenderer::setNode() was already adjusting a path's bounding rect by its stroke width, and we need a multiplicative factor there to account for cosmetic stroking, to avoid excessive clipping in the software renderer. So now we have another use for the triangulationScale that was introduced in bcfcaeb87be783d8c329b0bc96323f1c9863982d. When QQShapeGenericRenderer is used (rendererType == GeometryRenderer), and any ShapePath has cosmeticStroke, we need it to re-triangulate whenever scale changes. QQuickShapeGenericRenderer::triangulateStroke() calls QTriangulatingStroker::setInvScale(1 / triangulationScale), and QTriangulatingStroker::process() multiplies its m_width by the inverse scale that was set (since 2009). So this tells us that the intended meaning of triangulationScale is the inverse of the factor by which we multiply the pen width. And when QQShapeGenericRenderer is in use, and there are cosmetic strokes, QQuickShape::itemChange triggers re-triangulation on changes in scale. When setting the QQuickShapeCurveRenderer::DebugWireframe debug visualization flag, we need to repeat the vertex shader calculations to expand the "skinny" triangles according to stroke width, just as we do with the actual stroking vertices. For now customTriangulator2 remains as legacy code, to be removed later on. It's poorly named, and returns a list of TriangleData which need to be iterated afterwards ("fix it in post"), looking up the QQuadPath::Element again in that second loop, which can go wrong when a path contains a move command. (For example, it could calculate a bevel between the end-tangent of one subpath and the start-tangent of the next.) customTriangulator2() was called from only one place, processStroke(), to which addStrokeTriangleCallback() is given: so the new way is to just call the callback directly as soon as we've calculated each triangle. Because we are not iterating again afterwards, the switch(type) is not needed in that case, and we no longer need TriangleData::type, except for supporting customTriangulator2(). [ChangeLog][QtQuick][Shapes] ShapePath now has a cosmeticStroke property which causes strokeWidth to be constant despite scaling. Set the environment variable QT_QUICKSHAPES_STROKE_EXPANDING to 1 to enable an experimental method of expanding strokes in the vertex shader, minimizing the need to re-triangulate when strokeWidth changes. Task-number: QTBUG-124638 Change-Id: I4eac0ddcd6f623b79bc70c766ff116f4b77736cb Reviewed-by: Paul Olav Tvete <[email protected]> Reviewed-by: Eskil Abrahamsen Blomfeldt <[email protected]>
-rw-r--r--src/quick/CMakeLists.txt17
-rw-r--r--src/quick/scenegraph/qsgcurveglyphatlas.cpp3
-rw-r--r--src/quick/scenegraph/qsgcurveprocessor.cpp346
-rw-r--r--src/quick/scenegraph/qsgcurveprocessor_p.h8
-rw-r--r--src/quick/scenegraph/qsgcurvestrokenode.cpp37
-rw-r--r--src/quick/scenegraph/qsgcurvestrokenode_p.cpp16
-rw-r--r--src/quick/scenegraph/qsgcurvestrokenode_p.h25
-rw-r--r--src/quick/scenegraph/qsgcurvestrokenode_p_p.h39
-rw-r--r--src/quick/scenegraph/qsgrhiinternaltextnode.cpp5
-rw-r--r--src/quick/scenegraph/shaders_ng/shapestroke.frag22
-rw-r--r--src/quick/scenegraph/shaders_ng/shapestroke.vert53
-rw-r--r--src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag34
-rw-r--r--src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert52
-rw-r--r--src/quickshapes/qquickshape.cpp66
-rw-r--r--src/quickshapes/qquickshape_p.h5
-rw-r--r--src/quickshapes/qquickshape_p_p.h2
-rw-r--r--src/quickshapes/qquickshapecurverenderer.cpp179
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p.h1
-rw-r--r--src/quickshapes/qquickshapegenericrenderer.cpp9
-rw-r--r--src/quickshapes/qquickshapegenericrenderer_p.h1
-rw-r--r--src/quickshapes/qquickshapesoftwarerenderer.cpp17
-rw-r--r--src/quickshapes/qquickshapesoftwarerenderer_p.h3
-rw-r--r--src/quickshapes/shaders_ng/wireframe.vert1
-rw-r--r--src/quickvectorimage/generator/qquicknodeinfo_p.h2
-rw-r--r--tests/auto/quickshapes/qquickshape/tst_qquickshape.cpp12
-rw-r--r--tests/baseline/scenegraph/data/shape/caps_and_joins.qml137
-rw-r--r--tests/manual/painterpathquickshape/CMakeLists.txt1
-rw-r--r--tests/manual/painterpathquickshape/ControlPanel.qml28
-rw-r--r--tests/manual/painterpathquickshape/ControlledShape.qml3
-rw-r--r--tests/manual/painterpathquickshape/main.qml5
-rw-r--r--tests/manual/painterpathquickshape/regularAndCosmeticStrokes.qml92
-rw-r--r--tests/manual/vectorimagetest/data/styling/stroking_cosmetic.svg33
32 files changed, 1097 insertions, 157 deletions
diff --git a/src/quick/CMakeLists.txt b/src/quick/CMakeLists.txt
index 1f790fbb39..a2b5a39ce4 100644
--- a/src/quick/CMakeLists.txt
+++ b/src/quick/CMakeLists.txt
@@ -338,6 +338,8 @@ qt_internal_add_shaders(Quick "scenegraph_curve_shaders"
"scenegraph/shaders_ng/shapecurve.vert"
"scenegraph/shaders_ng/shapestroke.frag"
"scenegraph/shaders_ng/shapestroke.vert"
+ "scenegraph/shaders_ng/shapestroke_wireframe.frag"
+ "scenegraph/shaders_ng/shapestroke_wireframe.vert"
)
qt_internal_add_shaders(Quick "scenegraph_curve_shaders_derivatives"
@@ -361,6 +363,21 @@ qt_internal_add_shaders(Quick "scenegraph_curve_shaders_derivatives"
"scenegraph/shaders_ng/shapestroke_derivatives.vert.qsb"
)
+qt_internal_add_shaders(Quick "scenegraph_curve_shaders_expanding"
+ SILENT
+ BATCHABLE
+ PRECOMPILE
+ OPTIMIZED
+ MULTIVIEW
+ DEFINES "STROKE_EXPANDING"
+ PREFIX
+ "/qt-project.org"
+ FILES
+ "scenegraph/shaders_ng/shapestroke.vert"
+ OUTPUTS
+ "scenegraph/shaders_ng/shapestroke_expanding.vert.qsb"
+)
+
qt_internal_add_shaders(Quick "scenegraph_curve_shaders_lg"
SILENT
BATCHABLE
diff --git a/src/quick/scenegraph/qsgcurveglyphatlas.cpp b/src/quick/scenegraph/qsgcurveglyphatlas.cpp
index 17460e46ab..0d17597895 100644
--- a/src/quick/scenegraph/qsgcurveglyphatlas.cpp
+++ b/src/quick/scenegraph/qsgcurveglyphatlas.cpp
@@ -38,10 +38,11 @@ void QSGCurveGlyphAtlas::populate(const QList<glyph_t> &glyphs)
Glyph glyph;
- QSGCurveProcessor::processStroke(quadPath, 2, 2, Qt::MiterJoin, Qt::FlatCap,
+ QSGCurveProcessor::processStroke(quadPath, 2, 2, false, Qt::MiterJoin, Qt::FlatCap,
[&glyph](const std::array<QVector2D, 3> &s,
const std::array<QVector2D, 3> &p,
const std::array<QVector2D, 3> &n,
+ const std::array<float, 3> & /* extrusions */,
QSGCurveStrokeNode::TriangleFlags f) {
glyph.strokeVertices.append(s.at(0));
glyph.strokeVertices.append(s.at(1));
diff --git a/src/quick/scenegraph/qsgcurveprocessor.cpp b/src/quick/scenegraph/qsgcurveprocessor.cpp
index 1b9e803752..f51dc6d857 100644
--- a/src/quick/scenegraph/qsgcurveprocessor.cpp
+++ b/src/quick/scenegraph/qsgcurveprocessor.cpp
@@ -324,8 +324,9 @@ static bool isIntersecting(const QQuadPath &path, int e1, int e2, QList<std::pai
struct TriangleData
{
TrianglePoints points;
- int pathElementIndex = std::numeric_limits<int>::min();
TrianglePoints normals;
+ std::array<float, 3> extrusions;
+ int pathElementIndex = std::numeric_limits<int>::min();
private:
#ifndef QT_NO_DEBUG_STREAM
@@ -453,7 +454,8 @@ QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, const Q
int i0 = hull[0];
int i1 = hull[i];
int i2 = hull[i+1];
- ret.append({{pts[i0], pts[i1], pts[i2]}, elementIndex, {normals[i0], normals[i1], normals[i2]}});
+ ret.append({{pts[i0], pts[i1], pts[i2]}, {normals[i0], normals[i1], normals[i2]},
+ QSGCurveStrokeNode::defaultExtrusions(), elementIndex});
}
return ret;
}
@@ -548,11 +550,13 @@ static std::array<QVector2D, 5> calculateJoin(const QQuadPath::Element *element1
auto tooLong = [penFactor, length](QVector2D p1, QVector2D p2, QVector2D n) -> bool {
auto v = p2 - p1;
// It's too long if the projection onto the bisector is longer than the bisector
- // and the projection onto the normal to the bisector is shorter
- // than the pen margin (that projection is just v - proj)
+ // and half the projection (v - proj) onto the normal to the bisector
+ // (minus AA margin) is shorter than the pen factor.
// (We're also adding a 10% safety margin to length to make room for AA: not exact.)
auto projLen = QVector2D::dotProduct(v, n);
- return projLen * 0.9f < length && (v - n * projLen).length() * 0.9 < penFactor;
+ return projLen * 0.9f < length &&
+ (QSGCurveStrokeNode::expandingStrokeEnabled() ? ((v - n * projLen).length() - 2.0) * 0.5
+ : (v - n * projLen).length() * 0.9) < penFactor;
};
// The angle bisector of the tangent lines is not correct for curved lines. We could fix this by calculating
@@ -620,12 +624,12 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW
bool simpleCase = !checkTriangleOverlap(t1, t2);
if (simpleCase) {
- ret.append({{p1, p2, p5}, idx, {n1, n2, controlPointNormal}});
- ret.append({{p3, p4, p5}, idx, {n3, n4, controlPointNormal}});
+ ret.append({{p1, p2, p5}, {n1, n2, controlPointNormal}, {}, idx});
+ ret.append({{p3, p4, p5}, {n3, n4, controlPointNormal}, {}, idx});
if (controlPointOnRight) {
- ret.append({{p1, p3, p5}, idx, {n1, n3, controlPointNormal}});
+ ret.append({{p1, p3, p5}, {n1, n3, controlPointNormal}, {}, idx});
} else {
- ret.append({{p2, p4, p5}, idx, {n2, n4, controlPointNormal}});
+ ret.append({{p2, p4, p5}, {n2, n4, controlPointNormal}, {}, idx});
}
} else {
ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx));
@@ -740,9 +744,9 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW
} else if (squareCap) {
QVector2D c1 = p1 + capSpace;
QVector2D c2 = p2 + capSpace;
- ret.append({{p1, s, c1}, -1, {}});
- ret.append({{c1, s, c2}, -1, {}});
- ret.append({{p2, s, c2}, -1, {}});
+ ret.append({{p1, s, c1}, {}, {}, -1});
+ ret.append({{c1, s, c2}, {}, {}, -1});
+ ret.append({{p2, s, c2}, {}, {}, -1});
}
}
if (!nextElement) {
@@ -753,15 +757,15 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW
} else if (squareCap) {
QVector2D c3 = p3 + capSpace;
QVector2D c4 = p4 + capSpace;
- ret.append({{p3, e, c3}, -1, {}});
- ret.append({{c3, e, c4}, -1, {}});
- ret.append({{p4, e, c4}, -1, {}});
+ ret.append({{p3, e, c3}, {}, {}, -1});
+ ret.append({{c3, e, c4}, {}, {}, -1});
+ ret.append({{p4, e, c4}, {}, {}, -1});
}
}
if (element.isLine()) {
- ret.append({{p1, p2, p3}, i, {n1, n2, n3}});
- ret.append({{p2, p3, p4}, i, {n2, n3, n4}});
+ ret.append({{p1, p2, p3}, {n1, n2, n3}, {}, i});
+ ret.append({{p2, p3, p4}, {n2, n3, n4}, {}, i});
} else {
triangulateCurve(i, p1, p2, p3, p4, n1, n2, n3, n4);
}
@@ -777,27 +781,27 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW
//const auto inner = e + endInner * penFactor;
if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) {
- ret.append({{outer1, e, outer2}, -1, {}});
+ ret.append({{outer1, e, outer2}, {}, {}, -1});
} else if (roundJoin) {
- ret.append({{outer1, e, outer2}, i, {}});
+ ret.append({{outer1, e, outer2}, {}, {}, i});
QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penFactor;
if (!innerOnRight)
nn = -nn;
- ret.append({{outer1, outer1 + nn, outer2}, i, {}});
- ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {}});
+ ret.append({{outer1, outer1 + nn, outer2}, {}, {}, i});
+ ret.append({{outer1 + nn, outer2, outer2 + nn}, {}, {}, i});
} else if (miterJoin) {
QVector2D outer = e + outerB * penFactor;
- ret.append({{outer1, e, outer}, -2, {}});
- ret.append({{outer, e, outer2}, -2, {}});
+ ret.append({{outer1, e, outer}, {}, {}, -2});
+ ret.append({{outer, e, outer2}, {}, {}, -2});
}
if (!giveUpOnEndJoin) {
QVector2D inner = e + endInner * penFactor;
- ret.append({{inner, e, outer1}, i, {endInner, {}, endOuter}});
+ ret.append({{inner, e, outer1}, {endInner, {}, endOuter}, {}, i});
// The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet)
int nextIdx = addIdx(i, +1);
- ret.append({{inner, e, outer2}, nextIdx, {endInner, {}, nextOuter}});
+ ret.append({{inner, e, outer2}, {endInner, {}, nextOuter}, {}, nextIdx});
}
}
}
@@ -1565,24 +1569,30 @@ bool QSGCurveProcessor::solveIntersections(QQuadPath &path, bool removeNestedPat
void QSGCurveProcessor::processStroke(const QQuadPath &strokePath,
float miterLimit,
- float penWidth,
+ float penWidth, bool cosmetic,
Qt::PenJoinStyle joinStyle,
Qt::PenCapStyle capStyle,
addStrokeTriangleCallback addTriangle,
int subdivisions)
{
+ const bool expandingInVertexShader = cosmetic || QSGCurveStrokeNode::expandingStrokeEnabled();
auto thePath = subdivide(strokePath, subdivisions).flattened(); // TODO: don't flatten, but handle it in the triangulator
- auto triangles = customTriangulator2(thePath, penWidth, joinStyle, capStyle, miterLimit);
- qCDebug(lcSGCurveProcessor) << thePath << "->" << triangles;
auto addCurveTriangle = [&](const QQuadPath::Element &element, const TriangleData &t) {
+ qCDebug(lcSGCurveProcessor) << element << "->" << t;
QSGCurveStrokeNode::TriangleFlags flags;
flags.setFlag(QSGCurveStrokeNode::TriangleFlag::Line, element.isLine());
addTriangle(t.points,
{ element.startPoint(), element.controlPoint(), element.endPoint() },
- t.normals, flags);
+ t.normals, t.extrusions, flags);
};
+ if (!expandingInVertexShader) {
+ // this block is outdented to preserve git line history
+ // TODO remove customTriangulator2 in a future version
+ auto triangles = customTriangulator2(thePath, penWidth, joinStyle, capStyle, miterLimit);
+ qCDebug(lcSGCurveProcessor) << thePath << "->" << triangles;
+
auto addBevelTriangle = [&](const TrianglePoints &p, QSGCurveStrokeNode::TriangleFlags flags)
{
QVector2D fp1 = p[0];
@@ -1606,7 +1616,7 @@ void QSGCurveProcessor::processStroke(const QQuadPath &strokePath,
n[2] = (p[2] - p[1]).normalized();
flags.setFlag(QSGCurveStrokeNode::TriangleFlag::Line);
- addTriangle(p, { fp1, QVector2D(0.0f, 0.0f), fp2 }, n, flags);
+ addTriangle(p, { fp1, QVector2D(0.0f, 0.0f), fp2 }, n, {}, flags);
};
for (const auto &triangle : std::as_const(triangles)) {
@@ -1617,6 +1627,282 @@ void QSGCurveProcessor::processStroke(const QQuadPath &strokePath,
const auto &element = thePath.elementAt(triangle.pathElementIndex);
addCurveTriangle(element, triangle);
}
+
+ return;
+ } // legacy customTriangulator2 if we aren't using the expanding vertex shader
+
+ auto addEdgeTriangle = [&](QVector2D start, QVector2D end, const TriangleData &t) {
+ qCDebug(lcSGCurveProcessor) << "line from" << start << "to" << end << "->" << t;
+ addTriangle(t.points, { start, start, end }, t.normals, t.extrusions, QSGCurveStrokeNode::TriangleFlag::Line);
+ };
+
+ const bool bevelJoin = joinStyle == Qt::BevelJoin;
+ const bool roundJoin = joinStyle == Qt::RoundJoin;
+ const bool miterJoin = !bevelJoin && !roundJoin;
+
+ // Whether to allow a miter to simply be two adjacent triangle-pair stroke
+ // quads coming together at an angle: so far, it only works for round joins
+ // (and even then, only for suitably obtuse angles), because we need
+ // synthetic line-equation coefficients to get straight-edged joins.
+ // When we allow the user-provided endpoints to be visible inside the
+ // triangles, the fragment shader makes round endcaps.
+ const bool simpleMiter = joinStyle == Qt::RoundJoin;
+
+ Q_ASSERT(miterLimit > 0 || !miterJoin);
+ const float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0;
+ const float penFactor = penWidth / 2;
+
+ auto triangulateCurve = [&](int idx, const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, const QVector2D &p4,
+ const QVector2D &n1, const QVector2D &n2, const QVector2D &n3, const QVector2D &n4)
+ {
+ const auto &element = thePath.elementAt(idx);
+ Q_ASSERT(!element.isLine());
+ const auto &s = element.startPoint();
+ const auto &c = element.controlPoint();
+ const auto &e = element.endPoint();
+ // TODO: Don't flatten the path in addCurveStrokeNodes, but iterate over the children here instead
+ bool controlPointOnRight = determinant(s, c, e) > 0;
+ QVector2D startNormal = normalVector(element);
+ QVector2D endNormal = normalVector(element, true);
+ QVector2D controlPointNormal = (startNormal + endNormal).normalized();
+ if (controlPointOnRight)
+ controlPointNormal = -controlPointNormal;
+ TrianglePoints t1{p1, p2, c};
+ TrianglePoints t2{p3, p4, c};
+ bool simpleCase = !checkTriangleOverlap(t1, t2);
+ if (simpleCase) {
+ addCurveTriangle(element, {{p1, p2, c}, {n1, n2, controlPointNormal},
+ QSGCurveStrokeNode::defaultExtrusions(), idx});
+ addCurveTriangle(element, {{p3, p4, c}, {n3, n4, controlPointNormal},
+ QSGCurveStrokeNode::defaultExtrusions(), idx});
+ if (controlPointOnRight) {
+ addCurveTriangle(element, {{p1, p3, c}, {n1, n3, controlPointNormal},
+ QSGCurveStrokeNode::defaultExtrusions(), idx});
+ } else {
+ addCurveTriangle(element, {{p2, p4, c}, {n2, n4, controlPointNormal},
+ QSGCurveStrokeNode::defaultExtrusions(), idx});
+ }
+ } else {
+ const auto &triangles = simplePointTriangulator({p1, p2, c, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx);
+ for (const auto &triangle : triangles)
+ addCurveTriangle(element, triangle);
+ }
+ };
+
+ // Each element is calculated independently, so we don't have to special-case closed sub-paths.
+ // Take care so the end points of one element are precisely equal to the start points of the next.
+ // Any additional triangles needed for joining are added at the end of the current element.
+
+ const int count = thePath.elementCount();
+ int subStart = 0;
+ while (subStart < count) {
+ int subEnd = subStart;
+ for (int i = subStart + 1; i < count; ++i) {
+ const auto &e = thePath.elementAt(i);
+ if (e.isSubpathStart()) {
+ subEnd = i - 1;
+ break;
+ }
+ if (i == count - 1) {
+ subEnd = i;
+ break;
+ }
+ }
+ bool closed = thePath.elementAt(subStart).startPoint() == thePath.elementAt(subEnd).endPoint();
+ const int subCount = subEnd - subStart + 1;
+
+ auto addIdx = [&](int idx, int delta) -> int {
+ int subIdx = idx - subStart;
+ if (closed)
+ subIdx = (subIdx + subCount + delta) % subCount;
+ else
+ subIdx += delta;
+ return subStart + subIdx;
+ };
+ auto elementAt = [&](int idx, int delta) -> const QQuadPath::Element * {
+ int subIdx = idx - subStart;
+ if (closed) {
+ subIdx = (subIdx + subCount + delta) % subCount;
+ return &thePath.elementAt(subStart + subIdx);
+ }
+ subIdx += delta;
+ if (subIdx >= 0 && subIdx < subCount)
+ return &thePath.elementAt(subStart + subIdx);
+ return nullptr;
+ };
+
+ for (int i = subStart; i <= subEnd; ++i) {
+ const auto &element = thePath.elementAt(i);
+ const auto *nextElement = elementAt(i, +1);
+ const auto *prevElement = elementAt(i, -1);
+
+ const auto &s = element.startPoint();
+ const auto &e = element.endPoint();
+
+ bool startInnerIsRight;
+ bool startBisectorWithinMiterLimit; // Not used
+ bool giveUpOnStartJoin; // Not used
+ auto startJoin = calculateJoin(prevElement, &element,
+ penFactor, inverseMiterLimit, simpleMiter,
+ startBisectorWithinMiterLimit, startInnerIsRight,
+ giveUpOnStartJoin);
+ const QVector2D &startInner = startJoin[1];
+ const QVector2D &startOuter = startJoin[3];
+
+ bool endInnerIsRight;
+ bool endBisectorWithinMiterLimit;
+ bool giveUpOnEndJoin;
+ auto endJoin = calculateJoin(&element, nextElement,
+ penFactor, inverseMiterLimit, simpleMiter,
+ endBisectorWithinMiterLimit, endInnerIsRight,
+ giveUpOnEndJoin);
+ const QVector2D endInner = endJoin[0];
+ const QVector2D endOuter = endJoin[2];
+ const QVector2D nextOuter = endJoin[3];
+ const QVector2D outerBisector = endJoin[4];
+ const QVector2D startTangent = tangentVector(element, false).normalized();
+ const QVector2D endTangent = tangentVector(element, true).normalized();
+
+ QVector2D n1, n2, n3, n4;
+
+ if (startInnerIsRight) {
+ n1 = startInner;
+ n2 = startOuter;
+ } else {
+ n1 = startOuter;
+ n2 = startInner;
+ }
+
+ // repeat logic above for the other end:
+ if (endInnerIsRight) {
+ n3 = endInner;
+ n4 = endOuter;
+ } else {
+ n3 = endOuter;
+ n4 = endInner;
+ }
+
+ // When we fill triangles that make up square caps with fake lines,
+ // we need the line equations to extend way beyond the triangles,
+ // so that corner roundoff won't occur at any reasonable zoom level.
+ // But if the virtual lines are too long, AA quality suffers.
+ // Multiplying by 50 seems to get it to behave reasonably at zoom levels from 0.01 to 50.
+ static const float artificialLineExtension = 50;
+
+ // End cap triangles
+ if (capStyle != Qt::FlatCap) {
+ const QVector2D capNormalNone(0, 0);
+ // a cap is rendered in 3 triangles: in all cases below, the order is outer, middle, outer
+ if (!prevElement) {
+ // If the cap happens to be left-facing, "up" means in the -y direction,
+ // "down" is in the +y direction and so on; otherwise they are all rotated together
+ const QVector2D capNormalUp(startTangent.y(), -startTangent.x());
+ const QVector2D capNormalDown = -capNormalUp;
+ // hypoteneuses of triangles made from normalized vectors are longer: not re-normalized
+ const QVector2D capNormalUpOut = (capNormalUp - startTangent);
+ const QVector2D capNormalDownOut = (capNormalDown - startTangent); // not in the magic kingdom
+ Q_ASSERT(capNormalUpOut.length() == capNormalDownOut.length());
+ if (capStyle == Qt::RoundCap) {
+ addCurveTriangle(element, {{s, s, s}, {capNormalUp, capNormalNone, capNormalUpOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{s, s, s}, {capNormalUpOut, capNormalNone, capNormalDownOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{s, s, s}, {capNormalDown, capNormalNone, capNormalDownOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ } else { // SquareCap
+ addEdgeTriangle(element.startPoint(), element.startPoint() - startTangent * penWidth * artificialLineExtension,
+ {{s, s, s}, {capNormalUp, capNormalNone, capNormalUpOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ const auto norm = normalVector(element, false).normalized() * penWidth * artificialLineExtension;
+ addEdgeTriangle(element.startPoint() - norm, element.startPoint() + norm,
+ {{s, s, s}, {capNormalUpOut, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ addEdgeTriangle(element.startPoint(), element.startPoint() - startTangent * penWidth * artificialLineExtension,
+ {{s, s, s}, {capNormalDown, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ }
+ }
+ if (!nextElement) {
+ const QVector2D capNormalUp(endTangent.y(), -endTangent.x());
+ const QVector2D capNormalDown = -capNormalUp;
+ const QVector2D capNormalUpOut = (capNormalUp - endTangent);
+ const QVector2D capNormalDownOut = (capNormalDown - endTangent);
+ Q_ASSERT(capNormalUpOut.length() == capNormalDownOut.length());
+ if (capStyle == Qt::RoundCap) {
+ addCurveTriangle(element, {{e, e, e}, {capNormalDown, capNormalNone, capNormalDownOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{e, e, e}, {capNormalUpOut, capNormalNone, capNormalDownOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{e, e, e}, {capNormalUp, capNormalNone, capNormalUpOut},
+ QSGCurveStrokeNode::defaultExtrusions(), i});
+ } else { // SquareCap
+ addEdgeTriangle(element.endPoint() - endTangent * penWidth * artificialLineExtension, element.endPoint(),
+ {{e, e, e}, {capNormalDown, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ const auto norm = normalVector(element, true).normalized() * penWidth * artificialLineExtension;
+ addEdgeTriangle(element.endPoint() - norm, element.endPoint() + norm,
+ {{e, e, e}, {capNormalUpOut, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ addEdgeTriangle(element.endPoint() - endTangent * penWidth * artificialLineExtension, element.endPoint(),
+ {{e, e, e}, {capNormalUp, capNormalNone, capNormalUpOut}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ }
+ }
+ }
+
+ if (element.isLine()) {
+ addCurveTriangle(element, {{s, s, e}, {n1, n2, n3}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{s, e, e}, {n2, n3, n4}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ } else {
+ triangulateCurve(i, s, s, e, e, n1, n2, n3, n4);
+ }
+
+ bool trivialJoin = simpleMiter && endBisectorWithinMiterLimit && !giveUpOnEndJoin;
+ if (!trivialJoin && nextElement) {
+ // inside of join (opposite of bevel) is defined by triangle s, e, next.e
+ bool innerOnRight = endInnerIsRight;
+
+ const auto outer1 = e + endOuter;
+ const auto outer2 = e + nextOuter;
+ QVector2D nn = calcNormalVector(outer1, outer2).normalized();
+ if (!innerOnRight)
+ nn = -nn;
+ const QVector2D endOuterN = (outer1 - e).normalized();
+ const QVector2D nextOuterN = (outer2 - e).normalized();
+ const QVector2D endOuterBisectorN = (endOuterN + nn.normalized()).normalized();
+ const QVector2D nextOuterBisectorN = (nextOuterN + nn.normalized()).normalized();
+
+ if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) {
+ const float cosTheta = QVector2D::dotProduct(endOuterN, nextOuterN); // divided by magnitudes, which are 1s
+ const float cosHalf = cos(acos(cosTheta) / 2);
+ // draw a line from the end of element to the beginning of the next:
+ // slope is the average of the two tangents
+ const auto tan1 = endTangent * penWidth * artificialLineExtension;
+ const auto tan2 = tangentVector(*nextElement, false).normalized() * penWidth * artificialLineExtension;
+ const auto bevelTan = (tan2 - tan1) / 2;
+ // element.endPoint() is not the centerline of the bevel's stroke if full width, so
+ // we use extrusion (normalExt.z) in the vertex shader to adjust the stroke width so that its edge
+ // falls in the right place to draw a clean bevel within this triangle: i.e.
+ // compensate for the triangle becoming smaller as the join angle becomes more acute.
+ addEdgeTriangle(element.endPoint() - bevelTan, element.endPoint() + bevelTan,
+ {{e, e, e}, {endOuterN, {}, nextOuterN}, {cosHalf, cosHalf, cosHalf}, i});
+ } else if (roundJoin) {
+ addCurveTriangle(element, {{outer1, e, outer2}, {endOuterN, {}, nextOuterN}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{outer1, outer1 + nn, outer2}, {endOuterN, endOuterBisectorN, nextOuterN}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ addCurveTriangle(element, {{outer1 + nn, outer2, outer2 + nn}, {endOuterBisectorN, nextOuterN, nextOuterBisectorN}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ } else if (miterJoin) {
+ addEdgeTriangle(element.endPoint(), element.endPoint() - endTangent * penWidth * artificialLineExtension,
+ {{e, e, e}, {endOuterN, {}, outerBisector}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ addEdgeTriangle(nextElement->startPoint(),
+ nextElement->startPoint() - tangentVector(*nextElement, false).normalized() * penWidth * artificialLineExtension,
+ {{e, e, e}, {nextOuterN, {}, outerBisector}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ }
+
+ if (!giveUpOnEndJoin) {
+ addCurveTriangle(element, {{e, e, e}, {endInner, {}, endOuter}, QSGCurveStrokeNode::defaultExtrusions(), i});
+ // The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet)
+ int nextIdx = addIdx(i, +1);
+ addCurveTriangle(*nextElement, {{e, e, e}, {endInner, {}, nextOuter}, QSGCurveStrokeNode::defaultExtrusions(), nextIdx});
+ }
+ }
+ }
+ subStart = subEnd + 1;
+ }
}
// 2x variant of qHash(float)
diff --git a/src/quick/scenegraph/qsgcurveprocessor_p.h b/src/quick/scenegraph/qsgcurveprocessor_p.h
index b4f40b5431..bd25204a8a 100644
--- a/src/quick/scenegraph/qsgcurveprocessor_p.h
+++ b/src/quick/scenegraph/qsgcurveprocessor_p.h
@@ -32,9 +32,10 @@ public:
typedef std::function<void(const std::array<QVector2D, 3> &,
const std::array<QVector2D, 3> &,
uvForPointCallback)> addTriangleCallback;
- typedef std::function<void(const std::array<QVector2D, 3> &,
- const std::array<QVector2D, 3> &,
- const std::array<QVector2D, 3> &,
+ typedef std::function<void(const std::array<QVector2D, 3> &, // vertex coordinates
+ const std::array<QVector2D, 3> &, // start, control, end
+ const std::array<QVector2D, 3> &, // normals
+ const std::array<float, 3> &, // extrusions
QSGCurveStrokeNode::TriangleFlags)> addStrokeTriangleCallback;
static void processFill(const QQuadPath &path,
@@ -43,6 +44,7 @@ public:
static void processStroke(const QQuadPath &strokePath,
float miterLimit,
float penWidth,
+ bool cosmetic,
Qt::PenJoinStyle joinStyle,
Qt::PenCapStyle capStyle,
addStrokeTriangleCallback addTriangle,
diff --git a/src/quick/scenegraph/qsgcurvestrokenode.cpp b/src/quick/scenegraph/qsgcurvestrokenode.cpp
index f1f95d6cb2..be21eede78 100644
--- a/src/quick/scenegraph/qsgcurvestrokenode.cpp
+++ b/src/quick/scenegraph/qsgcurvestrokenode.cpp
@@ -11,12 +11,13 @@ QSGCurveStrokeNode::QSGCurveStrokeNode()
setFlag(OwnsGeometry, true);
qsgnode_set_description(this, QLatin1StringView("curve stroke"));
setGeometry(new QSGGeometry(attributes(), 0, 0));
- updateMaterial();
+ // defer updateMaterial() until later
}
void QSGCurveStrokeNode::QSGCurveStrokeNode::updateMaterial()
{
- m_material.reset(new QSGCurveStrokeMaterial(this));
+ const bool expandingInVertexShader = m_cosmetic || expandingStrokeEnabled();
+ m_material.reset(new QSGCurveStrokeMaterial(this, expandingInVertexShader));
setMaterial(m_material.data());
}
@@ -35,10 +36,17 @@ std::array<QVector2D, 3> QSGCurveStrokeNode::curveABC(const std::array<QVector2D
Append a triangle with \a vtx corners within which the fragment shader will
draw the visible part of a quadratic curve from ctl[0] to ctl[2] with
control point ctl[1] (AKA a quadratic Bézier curve with 3 control points).
+ The \a normal vectors are used in the vertex shader to expand the triangle
+ according to its stroke width: therefore, it's ok for the triangle to be
+ degenerate, and get expanded to size in the vertex shader. Normals are
+ usually unit vectors, but it's also ok for some to have larger magnitudes,
+ to handle the case when miter corners need to be extended proportionally
+ farther as stroke width increases.
*/
void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx,
const std::array<QVector2D, 3> &ctl,
- const std::array<QVector2D, 3> &normal)
+ const std::array<QVector2D, 3> &normal,
+ const std::array<float, 3> &extrusions)
{
auto abc = curveABC(ctl);
@@ -47,7 +55,7 @@ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx,
for (int i = 0; i < 3; ++i) {
m_uncookedVertexes.append( { vtx[i].x(), vtx[i].y(),
abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(),
- normal[i].x(), normal[i].y() } );
+ normal[i].x(), normal[i].y(), extrusions[i] } );
}
m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2;
}
@@ -55,13 +63,18 @@ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx,
/*!
Append a triangle with \a vtx corners within which the fragment shader will
draw the visible part of a line from ctl[0] to ctl[2].
+ The \a normal vectors are used in the vertex shader to expand the triangle
+ according to its stroke width: therefore, it's ok for the triangle to be
+ degenerate, and get expanded to size in the vertex shader. Normals are
+ usually unit vectors, but it's also ok for some to have larger magnitudes,
+ to handle the case when miter corners need to be extended proportionally
+ farther as stroke width increases.
*/
void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx,
const std::array<QVector2D, 2> &ctl,
const std::array<QVector2D, 3> &normal,
- QSGCurveStrokeNode::TriangleFlags flags)
+ const std::array<float, 3> &extrusions)
{
- Q_UNUSED(flags)
// 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
@@ -75,13 +88,14 @@ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx,
for (int i = 0; i < 3; ++i) {
m_uncookedVertexes.append( { vtx[i].x(), vtx[i].y(),
A.x(), A.y(), B.x(), B.y(), C.x(), C.y(),
- normal[i].x(), normal[i].y() } );
+ normal[i].x(), normal[i].y(), extrusions[i] } );
}
m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2;
}
void QSGCurveStrokeNode::cookGeometry()
{
+ updateMaterial(); // by now, setCosmeticStroke has been called if necessary
QSGGeometry *g = geometry();
if (g->indexType() != QSGGeometry::UnsignedIntType) {
g = new QSGGeometry(attributes(),
@@ -114,10 +128,17 @@ const QSGGeometry::AttributeSet &QSGCurveStrokeNode::attributes()
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), //normalVector
+ QSGGeometry::Attribute::createWithAttributeType(4, 3, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //normalExt
};
static QSGGeometry::AttributeSet attrs = { 5, sizeof(StrokeVertex), data };
return attrs;
}
+// TODO remove when we consider the expanding-stroke shader to be stable for full-time use
+bool QSGCurveStrokeNode::expandingStrokeEnabled()
+{
+ static const bool ret = qEnvironmentVariableIntValue("QT_QUICKSHAPES_STROKE_EXPANDING");
+ return ret;
+}
+
QT_END_NAMESPACE
diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p.cpp b/src/quick/scenegraph/qsgcurvestrokenode_p.cpp
index b86493b169..f9a92df99b 100644
--- a/src/quick/scenegraph/qsgcurvestrokenode_p.cpp
+++ b/src/quick/scenegraph/qsgcurvestrokenode_p.cpp
@@ -26,8 +26,11 @@ bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMate
m.scale(localScale);
memcpy(buf->data() + viewIndex * 64, m.constData(), 64);
}
+ // determinant is xscale * yscale, as long as Item.transform does not include shearing or rotation
float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio() * localScale;
memcpy(buf->data() + matrixCount * 64, &matrixScale, 4);
+ float dpr = state.devicePixelRatio();
+ memcpy(buf->data() + matrixCount * 64 + 8, &dpr, 4);
changed = true;
}
@@ -37,6 +40,12 @@ bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMate
changed = true;
}
+ if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) {
+ float w = newNode->strokeWidth();
+ memcpy(buf->data() + matrixCount * 64 + 12, &w, 4);
+ changed = true;
+ }
+
int offset = matrixCount * 64 + 16;
if (newNode == nullptr)
return changed;
@@ -58,18 +67,11 @@ bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMate
}
offset += 16;
- if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) {
- float w = newNode->strokeWidth();
- memcpy(buf->data() + offset, &w, 4);
- changed = true;
- }
- offset += 4;
if (oldNode == nullptr || newNode->debug() != oldNode->debug()) {
float w = newNode->debug();
memcpy(buf->data() + offset, &w, 4);
changed = true;
}
-// offset += 4;
return changed;
}
diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p.h b/src/quick/scenegraph/qsgcurvestrokenode_p.h
index 11fa72a2df..3c57a1debb 100644
--- a/src/quick/scenegraph/qsgcurvestrokenode_p.h
+++ b/src/quick/scenegraph/qsgcurvestrokenode_p.h
@@ -39,6 +39,12 @@ public:
return m_color;
}
+ void setCosmeticStroke(bool c)
+ {
+ m_cosmetic = c;
+ markDirty(DirtyMaterial);
+ }
+
void setStrokeWidth(float width)
{
m_strokeWidth = width;
@@ -47,26 +53,34 @@ public:
float strokeWidth() const
{
- return m_strokeWidth;
+ // Negative stroke width would not normally mean anything;
+ // here we use it to mean that the stroke is cosmetic.
+ return (m_cosmetic ? -1.0 : 1.0) * qAbs(m_strokeWidth);
}
enum class TriangleFlag {
+ None = 0,
Line = 1 << 0,
};
Q_DECLARE_FLAGS(TriangleFlags, TriangleFlag)
+ static constexpr std::array<float, 3> defaultExtrusions() { return {1.0f, 1.0f, 1.0f}; }
+
void appendTriangle(const std::array<QVector2D, 3> &vtx, // triangle vertices
const std::array<QVector2D, 3> &ctl, // curve points
- const std::array<QVector2D, 3> &normal); // vertex normals
+ const std::array<QVector2D, 3> &normal, // vertex normals
+ const std::array<float, 3> &extrusions = defaultExtrusions());
void appendTriangle(const std::array<QVector2D, 3> &vtx, // triangle vertices
const std::array<QVector2D, 2> &ctl, // line points
const std::array<QVector2D, 3> &normal, // vertex normals
- QSGCurveStrokeNode::TriangleFlags = {});
+ const std::array<float, 3> &extrusions = defaultExtrusions());
void cookGeometry() override;
static const QSGGeometry::AttributeSet &attributes();
+ static bool expandingStrokeEnabled();
+
QVector<quint32> uncookedIndexes() const
{
return m_uncookedIndexes;
@@ -100,14 +114,17 @@ private:
float ax, ay;
float bx, by;
float cx, cy;
- float nx, ny; //normal vector: direction to move vertext to account for AA
+ float nx, ny; // normal vector: direction to move vertex to account for AA
+ float extrusion; // stroke width multiplier (* uniform strokeWidth)
};
void updateMaterial();
static std::array<QVector2D, 3> curveABC(const std::array<QVector2D, 3> &p);
+ static const bool envStrokeExpanding;
QColor m_color;
+ ushort m_cosmetic = false; // packs alongside QColor; could be turned into flags if needed
float m_strokeWidth = 0.0f;
float m_debug = 0.0f;
float m_localScale = 1.0f;
diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p_p.h b/src/quick/scenegraph/qsgcurvestrokenode_p_p.h
index d5b8c7b2b1..8474062e5d 100644
--- a/src/quick/scenegraph/qsgcurvestrokenode_p_p.h
+++ b/src/quick/scenegraph/qsgcurvestrokenode_p_p.h
@@ -20,19 +20,29 @@
QT_BEGIN_NAMESPACE
+using namespace Qt::StringLiterals;
+
class QSGCurveStrokeNode;
class QSGCurveStrokeMaterial;
class Q_QUICK_EXPORT QSGCurveStrokeMaterialShader : public QSGMaterialShader
{
public:
- QSGCurveStrokeMaterialShader(bool useDerivatives, int viewCount)
+ enum class Variant { // flags
+ Default = 0,
+ Expanding = 0x01,
+ Derivatives = 0x02
+ };
+
+ QSGCurveStrokeMaterialShader(int variant, int viewCount)
{
- QString baseName(u":/qt-project.org/scenegraph/shaders_ng/shapestroke");
- if (useDerivatives)
- baseName += u"_derivatives";
- setShaderFileName(VertexStage, baseName + u".vert.qsb", viewCount);
- setShaderFileName(FragmentStage, baseName + u".frag.qsb", viewCount);
+ static constexpr auto baseName = u":/qt-project.org/scenegraph/shaders_ng/shapestroke"_sv;
+ setShaderFileName(VertexStage, baseName +
+ (variant & int(Variant::Expanding) ? u"_expanding"_sv : u""_sv)
+ + u".vert.qsb"_sv, viewCount);
+ setShaderFileName(FragmentStage, baseName +
+ (variant & int(Variant::Derivatives) ? u"_derivatives"_sv : u""_sv)
+ + u".frag.qsb"_sv, viewCount);
}
bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override;
@@ -42,8 +52,8 @@ public:
class Q_QUICK_EXPORT QSGCurveStrokeMaterial : public QSGMaterial
{
public:
- QSGCurveStrokeMaterial(QSGCurveStrokeNode *node)
- : m_node(node)
+ QSGCurveStrokeMaterial(QSGCurveStrokeNode *node, bool strokeExpanding = false)
+ : m_node(node), m_strokeExpanding(strokeExpanding)
{
setFlag(Blending, true);
}
@@ -58,15 +68,22 @@ public:
protected:
QSGMaterialType *type() const override
{
- static QSGMaterialType t;
- return &t;
+ static QSGMaterialType legacyType;
+ static QSGMaterialType strokeExpandingType;
+ return m_strokeExpanding ? &strokeExpandingType : &legacyType;
}
QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const override
{
- return new QSGCurveStrokeMaterialShader(renderMode == QSGRendererInterface::RenderMode3D, viewCount());
+ int variant = int(QSGCurveStrokeMaterialShader::Variant::Default);
+ if (m_strokeExpanding)
+ variant |= int(QSGCurveStrokeMaterialShader::Variant::Expanding);
+ if (renderMode == QSGRendererInterface::RenderMode3D)
+ variant |= int(QSGCurveStrokeMaterialShader::Variant::Derivatives);
+ return new QSGCurveStrokeMaterialShader(variant, viewCount());
}
QSGCurveStrokeNode *m_node;
+ bool m_strokeExpanding = false;
};
QT_END_NAMESPACE
diff --git a/src/quick/scenegraph/qsgrhiinternaltextnode.cpp b/src/quick/scenegraph/qsgrhiinternaltextnode.cpp
index 2bc2665784..cfbdfcaf9b 100644
--- a/src/quick/scenegraph/qsgrhiinternaltextnode.cpp
+++ b/src/quick/scenegraph/qsgrhiinternaltextnode.cpp
@@ -26,13 +26,14 @@ void QSGRhiInternalTextNode::addDecorationNode(const QRectF &rect, const QColor
path.moveTo(QVector2D(rect.left(), c.y()));
path.lineTo(QVector2D(rect.right(), c.y()));
- QSGCurveProcessor::processStroke(path, 2, rect.height(), Qt::MiterJoin, Qt::FlatCap,
+ QSGCurveProcessor::processStroke(path, 2, rect.height(), false, Qt::MiterJoin, Qt::FlatCap,
[&node](const std::array<QVector2D, 3> &s,
const std::array<QVector2D, 3> &p,
const std::array<QVector2D, 3> &n,
+ const std::array<float, 3> &ex,
QSGCurveStrokeNode::TriangleFlags f) {
Q_ASSERT(f.testFlag(QSGCurveStrokeNode::TriangleFlag::Line));
- node->appendTriangle(s, std::array<QVector2D, 2>{p.at(0), p.at(2)}, n);
+ node->appendTriangle(s, std::array<QVector2D, 2>{p.at(0), p.at(2)}, n, ex);
});
node->cookGeometry();
appendChildNode(node);
diff --git a/src/quick/scenegraph/shaders_ng/shapestroke.frag b/src/quick/scenegraph/shaders_ng/shapestroke.frag
index 3919438418..459fe0ad58 100644
--- a/src/quick/scenegraph/shaders_ng/shapestroke.frag
+++ b/src/quick/scenegraph/shaders_ng/shapestroke.frag
@@ -4,8 +4,7 @@ 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 = 4) in vec4 HGOW; // H and G: args to solveDepressedCubic(); O: offset; W: strokeWidth
layout(location = 0) out vec4 fragColor;
@@ -18,15 +17,15 @@ layout(std140, binding = 0) uniform buf {
float matrixScale;
float opacity;
- float reserved2;
- float reserved3;
+ float devicePixelRatio;
+ float strokeWidth;
vec4 strokeColor;
- float strokeWidth;
float debug;
float reserved5;
float reserved6;
+ float reserved7;
} ubuf;
float cuberoot(float x)
@@ -78,7 +77,8 @@ mat2 qInverse(mat2 matrix) {
void main()
{
- vec3 s = solveDepressedCubic(HG.x, HG.y) - vec3(offset, offset, offset);
+ float offset = HGOW.z;
+ vec3 s = solveDepressedCubic(HGOW.x, HGOW.y) - vec3(offset, offset, offset);
// choose the value of s that minimizes the distance from our pixel to the point on the curve
// stay in the logical coordinate system for now
@@ -93,8 +93,8 @@ void main()
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);
+ vec2 Q1 = (Qmin + HGOW.w / 2. * n);
+ vec2 Q2 = (Qmin - HGOW.w / 2. * n);
// Converting to screen coordinates:
#if defined(USE_DERIVATIVES)
@@ -136,9 +136,9 @@ void main()
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);
+ float centerline = step(HGOW.w * 0.01, dmin);
+ fillCoverage = fillCoverage * centerline + min(1., HGOW.w * ubuf.matrixScale) * (1. - centerline);
- fragColor = vec4(ubuf.strokeColor.rgb, 1.0) *ubuf.strokeColor.a * fillCoverage * ubuf.opacity
+ fragColor = vec4(ubuf.strokeColor.rgb, 1.0) * ubuf.strokeColor.a * fillCoverage * ubuf.opacity
+ ubuf.debug * vec4(0.0, 0.5, 1.0, 1.0) * (1.0 - fillCoverage) * ubuf.opacity;
}
diff --git a/src/quick/scenegraph/shaders_ng/shapestroke.vert b/src/quick/scenegraph/shaders_ng/shapestroke.vert
index e358e059eb..6e7161d391 100644
--- a/src/quick/scenegraph/shaders_ng/shapestroke.vert
+++ b/src/quick/scenegraph/shaders_ng/shapestroke.vert
@@ -1,18 +1,16 @@
#version 440
layout(location = 0) in vec4 vertexCoord;
-layout(location = 1) in vec2 inA;
+layout(location = 1) in vec2 inA; // A B and C: control points in logical coordinates
layout(location = 2) in vec2 inB;
layout(location = 3) in vec2 inC;
-layout(location = 4) in vec2 normalVector;
+layout(location = 4) in vec3 normalExt; // x and y: normal vector; z: strokeWidth multiplier (default 1)
-layout(location = 0) out vec4 P;
-layout(location = 1) out vec2 A;
+layout(location = 0) out vec4 P; // stroke edge
+layout(location = 1) out vec2 A; // A B and C: control points in logical coordinates
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(location = 4) out vec4 HGOW; // H and G: args to solveDepressedCubic(); O: offset; W: adj strokeWidth
layout(std140, binding = 0) uniform buf {
#if QSHADER_VIEW_COUNT >= 2
@@ -23,15 +21,15 @@ layout(std140, binding = 0) uniform buf {
float matrixScale;
float opacity;
- float reserved2;
- float reserved3;
+ float devicePixelRatio;
+ float strokeWidth;
vec4 strokeColor;
- float strokeWidth;
float debug;
float reserved5;
float reserved6;
+ float reserved7;
} ubuf;
#define SQRT2 1.41421356237
@@ -43,7 +41,33 @@ float qdot(vec2 a, vec2 b)
void main()
{
- P = vertexCoord + vec4(normalVector, 0.0, 0.0) * SQRT2/ubuf.matrixScale;
+ vec4 normalVector = vec4(normalExt.xy, 0.0, 0.0);
+ // If STROKE_EXPANDING is _not_ defined, vertexCoord starts out in
+ // logical coordinates, already positioned outwards past an edge of the stroke.
+ P = vertexCoord + normalVector * SQRT2 / ubuf.matrixScale;
+ float adjStrokeWidth = abs(ubuf.strokeWidth); // passed to fragment shader in HGOW.w
+
+#if defined(STROKE_EXPANDING)
+ // vertexCoord starts out in logical coordinates, in the center of the stroke.
+ // Move the vertex by stroke width * normalVector to fill the stroke width;
+ // and in case of a cosmetic stroke, divide by matrixScale to undo the scaling.
+ // In the case of a miter joint, the tip vertices need to be moved farther,
+ // so the length of normalVector may be > 1.
+ P += normalVector * adjStrokeWidth; // expand to stroke width
+
+ // Negative ubuf.strokeWidth means we want a "cosmetic pen", which means
+ // we make the stroke triangles wider or narrower to adjust for the difference
+ // between zoomed stroke width and cosmetic stroke width.
+ if (ubuf.strokeWidth < 0.) { // cosmetic stroke
+ adjStrokeWidth *= ubuf.devicePixelRatio / ubuf.matrixScale;
+ float strokeDiff = max(ubuf.devicePixelRatio / ubuf.matrixScale, adjStrokeWidth) *
+ (ubuf.matrixScale / ubuf.devicePixelRatio - 1) / 2.;
+ P -= normalVector * strokeDiff; // cosmetic adjustment
+ }
+
+ // adjust stroke width by the given multiplier (usually 1)
+ adjStrokeWidth *= normalExt.z;
+#endif // if the shader is expected to expand the stroke by moving vertices outwards
A = inA;
B = inB;
@@ -53,7 +77,7 @@ void main()
// 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:
+ // O (HGOW.z) is the static offset between s and t:
// s = t - b/(3a)
// use it to get back the parameter t
@@ -69,10 +93,7 @@ void main()
// both functions are linear in c and d and thus linear in p
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);
- HG = vec2(H, G);
- offset = b/(3*a);
-
-
+ HGOW = vec4(H, G, b / (3 * a), adjStrokeWidth);
#if QSHADER_VIEW_COUNT >= 2
gl_Position = ubuf.qt_Matrix[gl_ViewIndex] * P;
diff --git a/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag
new file mode 100644
index 0000000000..880904492e
--- /dev/null
+++ b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag
@@ -0,0 +1,34 @@
+#version 440
+
+layout(location = 0) in vec3 barycentric;
+
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf {
+#if QSHADER_VIEW_COUNT >= 2
+ mat4 qt_Matrix[QSHADER_VIEW_COUNT];
+#else
+ mat4 qt_Matrix;
+#endif
+
+ float matrixScale;
+ float opacity;
+ float devicePixelRatio;
+ float strokeWidth;
+
+ vec4 strokeColor;
+
+ float debug;
+ float reserved5;
+ float reserved6;
+ float reserved7;
+} ubuf;
+
+void main()
+{
+ float f = min(barycentric.x, min(barycentric.y, barycentric.z));
+ float d = fwidth(f * 1.5);
+ float alpha = smoothstep(0.0, d, f);
+
+ fragColor = vec4(1.0, 0.4, 0.1, 1.0) * (1.0 - alpha);
+}
diff --git a/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert
new file mode 100644
index 0000000000..7c014da82b
--- /dev/null
+++ b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert
@@ -0,0 +1,52 @@
+#version 440
+
+layout(location = 0) in vec4 vertexCoord;
+layout(location = 1) in vec3 vertexBarycentric;
+layout(location = 2) in vec3 normalExt; // x and y: normal vector; z: strokeWidth multiplier (default 1)
+layout(location = 0) out vec3 barycentric;
+
+layout(std140, binding = 0) uniform buf {
+#if QSHADER_VIEW_COUNT >= 2
+ mat4 qt_Matrix[QSHADER_VIEW_COUNT];
+#else
+ mat4 qt_Matrix;
+#endif
+
+ float matrixScale;
+ float opacity;
+ float devicePixelRatio;
+ float strokeWidth;
+
+ vec4 strokeColor;
+
+ float debug;
+ float reserved5;
+ float reserved6;
+ float reserved7;
+} ubuf;
+
+#define SQRT2 1.41421356237
+
+void main()
+{
+ // the subset of calculations from shapestroke.vert necessary to get gl_Position
+
+ vec4 normalV = vec4(normalExt.xy, 0.0, 0.0);
+ vec4 P = vertexCoord // center of stroke
+ + normalV * abs(ubuf.strokeWidth) // expand to stroke width
+ + normalV * SQRT2/ubuf.matrixScale; // AA
+
+ if (ubuf.strokeWidth < 0.) {
+ float adjStrokeWidth = abs(ubuf.strokeWidth) * ubuf.devicePixelRatio / ubuf.matrixScale;
+ float strokeDiff = max(ubuf.devicePixelRatio / ubuf.matrixScale, adjStrokeWidth) *
+ (ubuf.matrixScale / ubuf.devicePixelRatio - 1) / 2.;
+ P -= normalV * strokeDiff; // cosmetic adjustment
+ }
+
+ barycentric = vertexBarycentric;
+#if QSHADER_VIEW_COUNT >= 2
+ gl_Position = ubuf.qt_Matrix[gl_ViewIndex] * P;
+#else
+ gl_Position = ubuf.qt_Matrix * P;
+#endif
+}
diff --git a/src/quickshapes/qquickshape.cpp b/src/quickshapes/qquickshape.cpp
index ca7c3a1a6d..a000df524c 100644
--- a/src/quickshapes/qquickshape.cpp
+++ b/src/quickshapes/qquickshape.cpp
@@ -6,6 +6,7 @@
#include "qquickshapegenericrenderer_p.h"
#include "qquickshapesoftwarerenderer_p.h"
#include "qquickshapecurverenderer_p.h"
+#include <private/qsgcurvestrokenode_p.h>
#include <private/qsgplaintexture_p.h>
#include <private/qquicksvgparser_p.h>
#include <QtGui/private/qdrawhelper_p.h>
@@ -217,6 +218,8 @@ void QQuickShapePath::setStrokeColor(const QColor &color)
When set to a negative value, no stroking occurs.
The default value is 1.
+
+ \sa cosmeticStroke
*/
qreal QQuickShapePath::strokeWidth() const
@@ -236,6 +239,37 @@ void QQuickShapePath::setStrokeWidth(qreal w)
}
}
+/*! \since 6.11
+ \qmlproperty real QtQuick.Shapes::ShapePath::cosmeticStroke
+
+ This property holds whether the stroke width remains constant despite rendering scale.
+
+ When this property is set to \c true, the outline of the shape
+ is drawn with constant width in \l {High DPI}{device-independent pixels},
+ as specified by \l strokeWidth, regardless of any transformations applied
+ to the shape, such as \l QtQuick::Item::scale.
+
+ The default value is \c false.
+
+ \sa strokeWidth
+*/
+bool QQuickShapePath::cosmeticStroke() const
+{
+ Q_D(const QQuickShapePath);
+ return d->sfp.cosmeticStroke;
+}
+
+void QQuickShapePath::setCosmeticStroke(bool c)
+{
+ Q_D(QQuickShapePath);
+ if (d->sfp.cosmeticStroke != c) {
+ d->sfp.cosmeticStroke = c;
+ d->dirty |= QQuickShapePathPrivate::DirtyStrokeWidth;
+ emit cosmeticStrokeChanged();
+ emit shapePathChanged();
+ }
+}
+
/*!
\qmlproperty color QtQuick.Shapes::ShapePath::fillColor
@@ -1350,6 +1384,16 @@ void QQuickShape::itemChange(ItemChange change, const ItemChangeData &data)
QQuickShapePathPrivate::get(d->sp[i])->dirty = QQuickShapePathPrivate::DirtyAll;
d->_q_shapePathChanged();
d->handleSceneChange(data.window);
+ } else if (change == ItemTransformHasChanged && d->rendererType == QQuickShape::GeometryRenderer) {
+ bool cosmeticStrokeFound = false;
+ for (int i = 0; i < d->sp.size(); ++i) {
+ if (d->sp[i]->cosmeticStroke()) {
+ QQuickShapePathPrivate::get(d->sp[i])->dirty = QQuickShapePathPrivate::DirtyStrokeWidth;
+ cosmeticStrokeFound = true;
+ }
+ }
+ if (cosmeticStrokeFound)
+ d->_q_shapePathChanged();
}
QQuickItem::itemChange(change, data);
@@ -1450,6 +1494,9 @@ void QQuickShapePrivate::createRenderer()
rendererType = selectedType;
rendererChanged = true;
+ // If cosmetic stroking is used with GeometryRenderer, we need to be notified when the transform changes
+ q->setFlag(QQuickItem::ItemObservesViewport, rendererType == QQuickShape::GeometryRenderer);
+
switch (selectedType) {
case QQuickShape::SoftwareRenderer:
renderer = new QQuickShapeSoftwareRenderer;
@@ -1531,8 +1578,10 @@ void QQuickShapePrivate::sync()
const int count = sp.size();
bool countChanged = false;
+ const qreal det = windowToItemTransform().determinant();
+ const qreal adjTriangulationScale = triangulationScale /
+ (qIsNaN(det) || qIsNull(det) ? qreal(1) : qSqrt(qAbs(windowToItemTransform().determinant())));
renderer->beginSync(count, &countChanged);
- renderer->setTriangulationScale(triangulationScale);
qCDebug(lcShapeSync) << "syncing" << count << "path(s)";
for (int i = 0; i < count; ++i) {
@@ -1549,8 +1598,21 @@ void QQuickShapePrivate::sync()
qCDebug(lcShapeSync) << " - DirtyStrokeColor:" << p->strokeColor();
renderer->setStrokeColor(i, p->strokeColor());
}
- if (dirty & QQuickShapePathPrivate::DirtyStrokeWidth)
+ if (dirty & QQuickShapePathPrivate::DirtyStrokeWidth) {
+ // TODO adjust triangulationScale regardless of the env var, after we're satisfied that there are no significant regressions
+ if (p->cosmeticStroke() || QSGCurveStrokeNode::expandingStrokeEnabled()) {
+ renderer->setTriangulationScale(adjTriangulationScale);
+ qCDebug(lcShapeSync) << " - DirtyStrokeWidth:" << p->strokeWidth()
+ << "cosmetic:" << p->cosmeticStroke() << "triangulationScale"
+ << triangulationScale << "adjusted to" << adjTriangulationScale;
+ } else {
+ renderer->setTriangulationScale(triangulationScale);
+ qCDebug(lcShapeSync) << " - DirtyStrokeWidth:" << p->strokeWidth()
+ << "cosmetic:" << p->cosmeticStroke() << "triangulationScale" << triangulationScale;
+ }
renderer->setStrokeWidth(i, p->strokeWidth());
+ renderer->setCosmeticStroke(i, p->cosmeticStroke());
+ }
if (dirty & QQuickShapePathPrivate::DirtyFillColor)
renderer->setFillColor(i, p->fillColor());
if (dirty & QQuickShapePathPrivate::DirtyFillRule)
diff --git a/src/quickshapes/qquickshape_p.h b/src/quickshapes/qquickshape_p.h
index f0aedbabf0..f5193788b0 100644
--- a/src/quickshapes/qquickshape_p.h
+++ b/src/quickshapes/qquickshape_p.h
@@ -232,6 +232,7 @@ class Q_QUICKSHAPES_EXPORT QQuickShapePath : public QQuickPath
Q_PROPERTY(QMatrix4x4 fillTransform READ fillTransform WRITE setFillTransform NOTIFY fillTransformChanged REVISION(6, 8) FINAL)
Q_PROPERTY(QQuickItem *fillItem READ fillItem WRITE setFillItem NOTIFY fillItemChanged REVISION(6, 8) FINAL)
Q_PROPERTY(QQuickShapeTrim *trim READ trim CONSTANT REVISION(6, 10) FINAL)
+ Q_PROPERTY(bool cosmeticStroke READ cosmeticStroke WRITE setCosmeticStroke NOTIFY cosmeticStrokeChanged REVISION(6, 11) FINAL)
QML_NAMED_ELEMENT(ShapePath)
QML_ADDED_IN_VERSION(1, 0)
@@ -323,6 +324,9 @@ public:
QQuickShapeTrim *trim();
bool hasTrim() const;
+ bool cosmeticStroke() const;
+ void setCosmeticStroke(bool c);
+
Q_SIGNALS:
void shapePathChanged();
void strokeColorChanged();
@@ -339,6 +343,7 @@ Q_SIGNALS:
Q_REVISION(6, 7) void pathHintsChanged();
Q_REVISION(6, 8) void fillTransformChanged();
Q_REVISION(6, 8) void fillItemChanged();
+ Q_REVISION(6, 11) void cosmeticStrokeChanged();
private:
Q_DISABLE_COPY(QQuickShapePath)
diff --git a/src/quickshapes/qquickshape_p_p.h b/src/quickshapes/qquickshape_p_p.h
index de06e3f7a3..408e2d7651 100644
--- a/src/quickshapes/qquickshape_p_p.h
+++ b/src/quickshapes/qquickshape_p_p.h
@@ -51,6 +51,7 @@ public:
virtual void setPath(int index, const QPainterPath &path, QQuickShapePath::PathHints pathHints = {}) = 0;
virtual void setStrokeColor(int index, const QColor &color) = 0;
virtual void setStrokeWidth(int index, qreal w) = 0;
+ virtual void setCosmeticStroke(int index, bool c) = 0;
virtual void setFillColor(int index, const QColor &color) = 0;
virtual void setFillRule(int index, QQuickShapePath::FillRule fillRule) = 0;
virtual void setJoinStyle(int index, QQuickShapePath::JoinStyle joinStyle, int miterLimit) = 0;
@@ -95,6 +96,7 @@ struct QQuickShapeStrokeFillParams
int miterLimit;
QQuickShapePath::CapStyle capStyle;
QQuickShapePath::StrokeStyle strokeStyle;
+ bool cosmeticStroke = false;
qreal dashOffset;
QVector<qreal> dashPattern;
QQuickShapeGradient *fillGradient;
diff --git a/src/quickshapes/qquickshapecurverenderer.cpp b/src/quickshapes/qquickshapecurverenderer.cpp
index 548ed3eeb4..c3a4942dde 100644
--- a/src/quickshapes/qquickshapecurverenderer.cpp
+++ b/src/quickshapes/qquickshapecurverenderer.cpp
@@ -26,40 +26,36 @@ Q_LOGGING_CATEGORY(lcShapeCurveRenderer, "qt.shape.curverenderer");
namespace {
+/*! \internal
+ Choice of vertex shader to use for the wireframe node:
+ \li \c SimpleWFT is for when vertices are already in logical coordinates
+ \li \c StrokeWFT chooses the stroke shader, which moves vertices according to the stroke width uniform
+*/
+enum WireFrameType { SimpleWFT, StrokeWFT };
+
class QQuickShapeWireFrameMaterialShader : public QSGMaterialShader
{
public:
- QQuickShapeWireFrameMaterialShader(int viewCount)
+ QQuickShapeWireFrameMaterialShader(WireFrameType wft, int viewCount) : m_wftype(wft)
{
- setShaderFileName(VertexStage,
+ setShaderFileName(VertexStage, wft == StrokeWFT ?
+ QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shapestroke_wireframe.vert.qsb") :
QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.vert.qsb"), viewCount);
setShaderFileName(FragmentStage,
+ wft == StrokeWFT ?
+ QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shapestroke_wireframe.frag.qsb") :
QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.frag.qsb"), viewCount);
}
- bool updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *) override
- {
- bool changed = false;
- QByteArray *buf = state.uniformData();
- Q_ASSERT(buf->size() >= 64);
- const int matrixCount = qMin(state.projectionMatrixCount(), newMaterial->viewCount());
-
- for (int viewIndex = 0; viewIndex < matrixCount; ++viewIndex) {
- if (state.isMatrixDirty()) {
- const QMatrix4x4 m = state.combinedMatrix(viewIndex);
- memcpy(buf->data() + 64 * viewIndex, m.constData(), 64);
- changed = true;
- }
- }
+ bool updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *) override;
- return changed;
- }
+ WireFrameType m_wftype;
};
class QQuickShapeWireFrameMaterial : public QSGMaterial
{
public:
- QQuickShapeWireFrameMaterial()
+ QQuickShapeWireFrameMaterial(WireFrameType wft) : m_wftype(wft)
{
setFlag(Blending, true);
}
@@ -69,6 +65,21 @@ public:
return (type() - other->type());
}
+ void setCosmeticStroke(bool c)
+ {
+ m_cosmeticStroke = c;
+ }
+
+ void setStrokeWidth(float width)
+ {
+ m_strokeWidth = width;
+ }
+
+ float strokeWidth()
+ {
+ return (m_cosmeticStroke ? -1.0 : 1.0) * qAbs(m_strokeWidth);
+ }
+
protected:
QSGMaterialType *type() const override
{
@@ -77,17 +88,53 @@ protected:
}
QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override
{
- return new QQuickShapeWireFrameMaterialShader(viewCount());
+ return new QQuickShapeWireFrameMaterialShader(m_wftype, viewCount());
}
+ WireFrameType m_wftype;
+ bool m_cosmeticStroke = false;
+ float m_strokeWidth = 1.0f;
};
+bool QQuickShapeWireFrameMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *)
+{
+ QByteArray *buf = state.uniformData();
+ Q_ASSERT(buf->size() >= 64);
+ const int matrixCount = qMin(state.projectionMatrixCount(), newMaterial->viewCount());
+ bool changed = false;
+ float localScale = /* newNode != nullptr ? newNode->localScale() : */ 1.0f;
+
+ for (int viewIndex = 0; viewIndex < matrixCount; ++viewIndex) {
+ if (state.isMatrixDirty()) {
+ QMatrix4x4 m = state.combinedMatrix(viewIndex);
+ if (m_wftype == StrokeWFT)
+ m.scale(localScale);
+ memcpy(buf->data() + 64 * viewIndex, m.constData(), 64);
+ changed = true;
+ }
+ }
+ // determinant is xscale * yscale, as long as Item.transform does not include shearing or rotation
+ const float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio() * localScale;
+ memcpy(buf->data() + matrixCount * 64, &matrixScale, 4);
+ const float dpr = state.devicePixelRatio();
+ memcpy(buf->data() + matrixCount * 64 + 8, &dpr, 4);
+ const float opacity = 1.0; // don't fade the wireframe
+ memcpy(buf->data() + matrixCount * 64 + 4, &opacity, 4);
+ const float strokeWidth = static_cast<QQuickShapeWireFrameMaterial *>(newMaterial)->strokeWidth();
+ memcpy(buf->data() + matrixCount * 64 + 12, &strokeWidth, 4);
+ changed = true;
+ // shapestroke_wireframe.vert doesn't use the strokeColor and debug uniforms, so we don't bother setting them
+
+ return changed;
+}
+
+template <WireFrameType wftype>
class QQuickShapeWireFrameNode : public QSGCurveAbstractNode
{
public:
struct WireFrameVertex
{
- float x, y, u, v, w;
+ float x, y, u, v, w, nx, ny, sw;
};
QQuickShapeWireFrameNode()
@@ -103,9 +150,19 @@ public:
Q_UNUSED(col);
}
+ void setCosmeticStroke(bool c)
+ {
+ m_material->setCosmeticStroke(c);
+ }
+
+ void setStrokeWidth(float width)
+ {
+ m_material->setStrokeWidth(width);
+ }
+
void activateMaterial()
{
- m_material.reset(new QQuickShapeWireFrameMaterial);
+ m_material.reset(new QQuickShapeWireFrameMaterial(wftype));
setMaterial(m_material.data());
}
@@ -114,8 +171,9 @@ public:
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, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
};
- static QSGGeometry::AttributeSet attrs = { 2, sizeof(WireFrameVertex), data };
+ static QSGGeometry::AttributeSet attrs = { 3, sizeof(WireFrameVertex), data };
return attrs;
}
@@ -182,6 +240,13 @@ void QQuickShapeCurveRenderer::setStrokeWidth(int index, qreal w)
pathData.m_dirty |= StrokeDirty;
}
+void QQuickShapeCurveRenderer::setCosmeticStroke(int index, bool c)
+{
+ auto &pathData = m_paths[index];
+ pathData.pen.setCosmetic(c);
+ pathData.m_dirty |= StrokeDirty;
+}
+
void QQuickShapeCurveRenderer::setFillColor(int index, const QColor &color)
{
auto &pathData = m_paths[index];
@@ -419,8 +484,18 @@ void QQuickShapeCurveRenderer::updateNode()
? pathData.fillTextureProviderItem->textureProvider()
: nullptr);
}
- for (auto &strokeNode : std::as_const(pathData.strokeNodes))
- strokeNode->setColor(pathData.pen.color());
+ for (QSGCurveAbstractNode *pathNode : std::as_const(pathData.strokeNodes)) {
+ pathNode->setColor(pathData.pen.color());
+ if (pathNode->isDebugNode) {
+ auto *wfNode = static_cast<QQuickShapeWireFrameNode<StrokeWFT> *>(pathNode);
+ wfNode->setStrokeWidth(pathData.pen.widthF());
+ wfNode->setCosmeticStroke(pathData.pen.isCosmetic());
+ } else {
+ auto *strokeNode = static_cast<QSGCurveStrokeNode *>(pathNode);
+ strokeNode->setStrokeWidth(pathData.pen.widthF());
+ strokeNode->setCosmeticStroke(pathData.pen.isCosmetic());
+ }
+ }
};
NodeList toBeDeleted;
@@ -551,7 +626,7 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addFillNodes(const
{
NodeList ret;
std::unique_ptr<QSGCurveFillNode> node(new QSGCurveFillNode);
- std::unique_ptr<QQuickShapeWireFrameNode> wfNode;
+ std::unique_ptr<QQuickShapeWireFrameNode<SimpleWFT>> wfNode;
const qsizetype approxDataCount = 20 * path.elementCount();
node->reserve(approxDataCount);
@@ -569,7 +644,7 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addFillNodes(const
node->appendTriangle(v, n, uvForPoint);
});
} else {
- QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
+ QVector<QQuickShapeWireFrameNode<SimpleWFT>::WireFrameVertex> wfVertices;
wfVertices.reserve(approxDataCount);
QSGCurveProcessor::processFill(path,
path.fillRule(),
@@ -579,14 +654,14 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addFillNodes(const
{
node->appendTriangle(v, n, uvForPoint);
- wfVertices.append({v.at(0).x(), v.at(0).y(), 1.0f, 0.0f, 0.0f}); // 0
- wfVertices.append({v.at(1).x(), v.at(1).y(), 0.0f, 1.0f, 0.0f}); // 1
- wfVertices.append({v.at(2).x(), v.at(2).y(), 0.0f, 0.0f, 1.0f}); // 2
+ wfVertices.append({v.at(0).x(), v.at(0).y(), 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }); // 0
+ wfVertices.append({v.at(1).x(), v.at(1).y(), 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }); // 1
+ wfVertices.append({v.at(2).x(), v.at(2).y(), 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }); // 2
});
- wfNode.reset(new QQuickShapeWireFrameNode);
+ wfNode.reset(new QQuickShapeWireFrameNode<SimpleWFT>);
const QVector<quint32> indices = node->uncookedIndexes();
- QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
+ QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode<SimpleWFT>::attributes(),
wfVertices.size(),
indices.size(),
QSGGeometry::UnsignedIntType);
@@ -619,7 +694,7 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStr
NodeList ret;
const QColor &color = pen.color();
- QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
+ QVector<QQuickShapeWireFrameNode<StrokeWFT>::WireFrameVertex> wfVertices;
QTriangulatingStroker stroker;
const auto painterPath = path.toPainterPath();
@@ -673,9 +748,9 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStr
node->appendTriangle(p1, p2, p3, uvForPoint);
- 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
+ wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f}); // 0
+ wfVertices.append({p2.x(), p2.y(), 0.0f, 0.1f, 0.0f, 0.0f, 0.0f, 1.0f}); // 1
+ wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f}); // 2
if (!disableExtraTriangles) {
// Add a triangle on the outer side of the line to get some more AA
@@ -683,9 +758,9 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStr
QVector2D op = findPointOtherSide(p1, p3, p2);
node->appendTriangle(p1, op, p3, uvForPoint);
- 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
- wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f});
+ wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f});
+ wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}); // replacing p2
+ wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f});
}
};
@@ -708,8 +783,8 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStr
}
const bool wireFrame = debugVisualization() & DebugWireframe;
if (wireFrame) {
- QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
- QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
+ QQuickShapeWireFrameNode<StrokeWFT> *wfNode = new QQuickShapeWireFrameNode<StrokeWFT>;
+ QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode<StrokeWFT>::attributes(),
wfVertices.size(),
indices.size(),
QSGGeometry::UnsignedIntType);
@@ -770,7 +845,7 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addCurveStrokeNodes
const bool debug = debugVisualization() & DebugCurves;
auto *node = new QSGCurveStrokeNode;
node->setDebug(0.2f * debug);
- QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
+ QVector<QQuickShapeWireFrameNode<StrokeWFT>::WireFrameVertex> wfVertices;
const float penWidth = pen.widthF();
@@ -779,27 +854,28 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addCurveStrokeNodes
const bool wireFrame = debugVisualization() & DebugWireframe;
QSGCurveProcessor::processStroke(path,
pen.miterLimit(),
- penWidth,
+ penWidth, pen.isCosmetic(),
pen.joinStyle(),
pen.capStyle(),
// addStrokeTriangleCallback (see qsgcurveprocessor_p.h):
[&wfVertices, &node, &wireFrame](const std::array<QVector2D, 3> &vtx, // triangle corners
const std::array<QVector2D, 3> &ctl, // curve control points
const std::array<QVector2D, 3> &n, // normals
+ const std::array<float, 3> &ex, // extrusions
QSGCurveStrokeNode::TriangleFlags flags)
{
const QVector2D &v0 = vtx.at(0);
const QVector2D &v1 = vtx.at(1);
const QVector2D &v2 = vtx.at(2);
if (flags.testFlag(QSGCurveStrokeNode::TriangleFlag::Line))
- node->appendTriangle(vtx, std::array<QVector2D, 2>{ctl.at(0), ctl.at(2)}, n, flags);
+ node->appendTriangle(vtx, std::array<QVector2D, 2>{ctl.at(0), ctl.at(2)}, n, ex);
else
- node->appendTriangle(vtx, ctl, n);
+ node->appendTriangle(vtx, ctl, n, ex);
if (Q_UNLIKELY(wireFrame)) {
- wfVertices.append({v0.x(), v0.y(), 1.0f, 0.0f, 0.0f});
- wfVertices.append({v1.x(), v1.y(), 0.0f, 1.0f, 0.0f});
- wfVertices.append({v2.x(), v2.y(), 0.0f, 0.0f, 1.0f});
+ wfVertices.append({v0.x(), v0.y(), 1.0f, 0.0f, 0.0f, n.at(0).x(), n.at(0).y(), ex.at(0)});
+ wfVertices.append({v1.x(), v1.y(), 0.0f, 1.0f, 0.0f, n.at(1).x(), n.at(1).y(), ex.at(1)});
+ wfVertices.append({v2.x(), v2.y(), 0.0f, 0.0f, 1.0f, n.at(2).x(), n.at(2).y(), ex.at(2)});
}
},
subdivisions);
@@ -808,17 +884,20 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addCurveStrokeNodes
node->setColor(pen.color());
node->setStrokeWidth(penWidth);
+ node->setCosmeticStroke(pen.isCosmetic());
node->cookGeometry();
ret.append(node);
if (Q_UNLIKELY(wireFrame)) {
- QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
+ QQuickShapeWireFrameNode<StrokeWFT> *wfNode = new QQuickShapeWireFrameNode<StrokeWFT>;
- QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
+ QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode<StrokeWFT>::attributes(),
wfVertices.size(),
indexCopy.size(),
QSGGeometry::UnsignedIntType);
wfNode->setGeometry(wfg);
+ wfNode->setCosmeticStroke(pen.isCosmetic());
+ wfNode->setStrokeWidth(penWidth);
wfg->setDrawingMode(QSGGeometry::DrawTriangles);
memcpy(wfg->indexData(),
diff --git a/src/quickshapes/qquickshapecurverenderer_p.h b/src/quickshapes/qquickshapecurverenderer_p.h
index 621ef831aa..b5293e9b47 100644
--- a/src/quickshapes/qquickshapecurverenderer_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p.h
@@ -47,6 +47,7 @@ public:
void setPath(int index, const QPainterPath &path, QQuickShapePath::PathHints pathHints = {}) override;
void setStrokeColor(int index, const QColor &color) override;
void setStrokeWidth(int index, qreal w) override;
+ void setCosmeticStroke(int index, bool c) override;
void setFillColor(int index, const QColor &color) override;
void setFillRule(int index, QQuickShapePath::FillRule fillRule) override;
void setJoinStyle(int index, QQuickShapePath::JoinStyle joinStyle, int miterLimit) override;
diff --git a/src/quickshapes/qquickshapegenericrenderer.cpp b/src/quickshapes/qquickshapegenericrenderer.cpp
index e7a8d00867..22363bf222 100644
--- a/src/quickshapes/qquickshapegenericrenderer.cpp
+++ b/src/quickshapes/qquickshapegenericrenderer.cpp
@@ -156,6 +156,15 @@ void QQuickShapeGenericRenderer::setStrokeWidth(int index, qreal w)
d.syncDirty |= DirtyStrokeGeom;
}
+void QQuickShapeGenericRenderer::setCosmeticStroke(int index, bool c)
+{
+ ShapePathData &d(m_sp[index]);
+ d.pen.setCosmetic(c);
+ d.syncDirty |= DirtyStrokeGeom;
+ // as long as the stroke is cosmetic,
+ // QQuickShape::itemChange triggers re-triangulation whenever scale changes
+}
+
void QQuickShapeGenericRenderer::setFillColor(int index, const QColor &color)
{
ShapePathData &d(m_sp[index]);
diff --git a/src/quickshapes/qquickshapegenericrenderer_p.h b/src/quickshapes/qquickshapegenericrenderer_p.h
index e6a0fadafe..09eb5cee97 100644
--- a/src/quickshapes/qquickshapegenericrenderer_p.h
+++ b/src/quickshapes/qquickshapegenericrenderer_p.h
@@ -59,6 +59,7 @@ public:
void setPath(int index, const QPainterPath &path, QQuickShapePath::PathHints pathHints = {}) override;
void setStrokeColor(int index, const QColor &color) override;
void setStrokeWidth(int index, qreal w) override;
+ void setCosmeticStroke(int index, bool c) override;
void setFillColor(int index, const QColor &color) override;
void setFillRule(int index, QQuickShapePath::FillRule fillRule) override;
void setJoinStyle(int index, QQuickShapePath::JoinStyle joinStyle, int miterLimit) override;
diff --git a/src/quickshapes/qquickshapesoftwarerenderer.cpp b/src/quickshapes/qquickshapesoftwarerenderer.cpp
index 7051b1e6e4..41b4949d8d 100644
--- a/src/quickshapes/qquickshapesoftwarerenderer.cpp
+++ b/src/quickshapes/qquickshapesoftwarerenderer.cpp
@@ -43,6 +43,14 @@ void QQuickShapeSoftwareRenderer::setStrokeWidth(int index, qreal w)
m_accDirty |= DirtyPen;
}
+void QQuickShapeSoftwareRenderer::setCosmeticStroke(int index, bool c)
+{
+ ShapePathGuiData &d(m_sp[index]);
+ d.pen.setCosmetic(c);
+ d.dirty |= DirtyPen;
+ m_accDirty |= DirtyPen;
+}
+
void QQuickShapeSoftwareRenderer::setFillColor(int index, const QColor &color)
{
ShapePathGuiData &d(m_sp[index]);
@@ -159,6 +167,11 @@ void QQuickShapeSoftwareRenderer::setFillTransform(int index, const QSGTransform
m_accDirty |= DirtyBrush;
}
+void QQuickShapeSoftwareRenderer::setTriangulationScale(qreal scale)
+{
+ m_triangulationScale = scale;
+}
+
void QQuickShapeSoftwareRenderer::endSync(bool)
{
}
@@ -204,7 +217,9 @@ void QQuickShapeSoftwareRenderer::updateNode()
src.dirty = 0;
QRectF br = dst.path.boundingRect();
- const float sw = qMax(1.0f, dst.strokeWidth);
+ float sw = qMax(1.0f, dst.strokeWidth);
+ if (dst.pen.isCosmetic())
+ sw *= 2.0f / m_triangulationScale;
br.adjust(-sw, -sw, sw, sw);
m_node->m_boundingRect |= br;
}
diff --git a/src/quickshapes/qquickshapesoftwarerenderer_p.h b/src/quickshapes/qquickshapesoftwarerenderer_p.h
index 67a54151fe..bb071e07e0 100644
--- a/src/quickshapes/qquickshapesoftwarerenderer_p.h
+++ b/src/quickshapes/qquickshapesoftwarerenderer_p.h
@@ -40,6 +40,7 @@ public:
void setPath(int index, const QPainterPath &path, QQuickShapePath::PathHints pathHints = {}) override;
void setStrokeColor(int index, const QColor &color) override;
void setStrokeWidth(int index, qreal w) override;
+ void setCosmeticStroke(int index, bool c) override;
void setFillColor(int index, const QColor &color) override;
void setFillRule(int index, QQuickShapePath::FillRule fillRule) override;
void setJoinStyle(int index, QQuickShapePath::JoinStyle joinStyle, int miterLimit) override;
@@ -49,6 +50,7 @@ public:
void setFillGradient(int index, QQuickShapeGradient *gradient) override;
void setFillTextureProvider(int index, QQuickItem *textureProviderItem) override;
void setFillTransform(int index, const QSGTransform &transform) override;
+ void setTriangulationScale(qreal scale) override;
void endSync(bool async) override;
void handleSceneChange(QQuickWindow *window) override;
@@ -69,6 +71,7 @@ private:
Qt::FillRule fillRule;
};
QVector<ShapePathGuiData> m_sp;
+ float m_triangulationScale = 1.0f;
};
class QQuickShapeSoftwareRenderNode : public QSGRenderNode
diff --git a/src/quickshapes/shaders_ng/wireframe.vert b/src/quickshapes/shaders_ng/wireframe.vert
index 69f8872d51..52fa8b711f 100644
--- a/src/quickshapes/shaders_ng/wireframe.vert
+++ b/src/quickshapes/shaders_ng/wireframe.vert
@@ -2,6 +2,7 @@
layout(location = 0) in vec4 vertexCoord;
layout(location = 1) in vec3 vertexBarycentric;
+layout(location = 2) in vec3 normalExt; // x and y: normal vector; z: strokeWidth multiplier (default 1)
layout(location = 0) out vec3 barycentric;
layout(std140, binding = 0) uniform buf {
diff --git a/src/quickvectorimage/generator/qquicknodeinfo_p.h b/src/quickvectorimage/generator/qquicknodeinfo_p.h
index 468f20117e..e4691572cb 100644
--- a/src/quickvectorimage/generator/qquicknodeinfo_p.h
+++ b/src/quickvectorimage/generator/qquicknodeinfo_p.h
@@ -54,6 +54,7 @@ struct StrokeStyle
Qt::PenCapStyle lineCapStyle = Qt::SquareCap;
Qt::PenJoinStyle lineJoinStyle = Qt::MiterJoin;
int miterLimit = 4;
+ bool cosmetic = false;
qreal dashOffset = 0;
QList<qreal> dashArray;
QQuickAnimatedProperty color = QQuickAnimatedProperty(QVariant::fromValue(QColorConstants::Transparent));
@@ -66,6 +67,7 @@ struct StrokeStyle
style.lineCapStyle = p.capStyle();
style.lineJoinStyle = p.joinStyle() == Qt::SvgMiterJoin ? Qt::MiterJoin : p.joinStyle(); //TODO support SvgMiterJoin
style.miterLimit = qRound(p.miterLimit());
+ style.cosmetic = p.isCosmetic();
style.dashOffset = p.dashOffset();
style.dashArray = p.dashPattern();
style.width = p.widthF();
diff --git a/tests/auto/quickshapes/qquickshape/tst_qquickshape.cpp b/tests/auto/quickshapes/qquickshape/tst_qquickshape.cpp
index e37242de67..237560762f 100644
--- a/tests/auto/quickshapes/qquickshape/tst_qquickshape.cpp
+++ b/tests/auto/quickshapes/qquickshape/tst_qquickshape.cpp
@@ -147,6 +147,7 @@ void tst_QQuickShape::vpInitValues()
QCOMPARE(pathList.count(), 0);
QCOMPARE(vp->strokeColor(), QColor(Qt::white));
QCOMPARE(vp->strokeWidth(), 1.0f);
+ QCOMPARE(vp->cosmeticStroke(), false);
QCOMPARE(vp->fillColor(), QColor(Qt::white));
QCOMPARE(vp->fillRule(), QQuickShapePath::OddEvenFill);
QCOMPARE(vp->joinStyle(), QQuickShapePath::BevelJoin);
@@ -174,11 +175,22 @@ void tst_QQuickShape::basicShape()
QQuickShapePath *vp = qobject_cast<QQuickShapePath *>(list.at(0));
QVERIFY(vp != nullptr);
QCOMPARE(vp->strokeWidth(), 4.0f);
+ QCOMPARE(vp->cosmeticStroke(), false);
QVERIFY(vp->fillGradient() != nullptr);
QCOMPARE(vp->strokeStyle(), QQuickShapePath::DashLine);
vp->setStrokeWidth(5.0f);
QCOMPARE(vp->strokeWidth(), 5.0f);
+ vp->setStrokeWidth(-5.0f);
+ QCOMPARE(vp->strokeWidth(), -5.0f); // but it won't render
+ QCOMPARE(vp->cosmeticStroke(), false);
+ vp->setCosmeticStroke(true);
+ QCOMPARE(vp->cosmeticStroke(), true);
+ QCOMPARE(vp->strokeWidth(), -5.0f);
+ vp->setStrokeWidth(5.0f);
+ QCOMPARE(vp->strokeWidth(), 5.0f);
+ QCOMPARE(vp->strokeWidth(), 5.0f);
+ QCOMPARE(vp->cosmeticStroke(), true);
QQuickShapeLinearGradient *lgrad = qobject_cast<QQuickShapeLinearGradient *>(vp->fillGradient());
QVERIFY(lgrad != nullptr);
diff --git a/tests/baseline/scenegraph/data/shape/caps_and_joins.qml b/tests/baseline/scenegraph/data/shape/caps_and_joins.qml
new file mode 100644
index 0000000000..003d14efea
--- /dev/null
+++ b/tests/baseline/scenegraph/data/shape/caps_and_joins.qml
@@ -0,0 +1,137 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Shapes
+
+Item {
+ width: 1500; height: 1350
+
+ component Shapes: Shape {
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 10
+ capStyle: ShapePath.RoundCap
+ joinStyle: ShapePath.RoundJoin
+ PathMove { x: 160; y: 100 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: -25; relativeControlY: -25 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: 50; relativeControlY: -25 }
+ PathCubic { relativeX: 0; relativeY: 300;
+ relativeControl1X: -250; relativeControl1Y: 100
+ relativeControl2X: 250; relativeControl2Y: 200 }
+ PathLine { relativeX: -150; relativeY: -250 }
+ PathLine { relativeX: 75; relativeY: 200 }
+ PathLine { relativeX: -75; relativeY: 0 }
+ }
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 10
+ capStyle: ShapePath.SquareCap
+ joinStyle: ShapePath.MiterJoin
+ PathMove { x: 160; y: 450 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: -25; relativeControlY: -25 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: 50; relativeControlY: -25 }
+ PathCubic { relativeX: 0; relativeY: 300;
+ relativeControl1X: -250; relativeControl1Y: 100
+ relativeControl2X: 250; relativeControl2Y: 200 }
+ PathLine { relativeX: -150; relativeY: -250 }
+ PathLine { relativeX: 75; relativeY: 200 }
+ PathLine { relativeX: -75; relativeY: 0 }
+ }
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 10
+ capStyle: ShapePath.FlatCap
+ joinStyle: ShapePath.BevelJoin
+ PathMove { x: 160; y: 800 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: -25; relativeControlY: -25 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: 50; relativeControlY: -25 }
+ PathCubic { relativeX: 0; relativeY: 300;
+ relativeControl1X: -250; relativeControl1Y: 100
+ relativeControl2X: 250; relativeControl2Y: 200 }
+ PathLine { relativeX: -150; relativeY: -250 }
+ PathLine { relativeX: 75; relativeY: 200 }
+ PathLine { relativeX: -75; relativeY: 0 }
+ }
+
+ // cosmetic stroke, scale = 1
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 10
+ cosmeticStroke: true
+ capStyle: ShapePath.RoundCap
+ joinStyle: ShapePath.RoundJoin
+ PathMove { x: 410; y: 100 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: -25; relativeControlY: -25 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: 50; relativeControlY: -25 }
+ PathCubic { relativeX: 0; relativeY: 300;
+ relativeControl1X: -250; relativeControl1Y: 100
+ relativeControl2X: 250; relativeControl2Y: 200 }
+ PathLine { relativeX: -150; relativeY: -250 }
+ PathLine { relativeX: 75; relativeY: 200 }
+ PathLine { relativeX: -75; relativeY: 0 }
+ }
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 10
+ cosmeticStroke: true
+ capStyle: ShapePath.SquareCap
+ joinStyle: ShapePath.MiterJoin
+ PathMove { x: 410; y: 450 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: -25; relativeControlY: -25 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: 50; relativeControlY: -25 }
+ PathCubic { relativeX: 0; relativeY: 300;
+ relativeControl1X: -250; relativeControl1Y: 100
+ relativeControl2X: 250; relativeControl2Y: 200 }
+ PathLine { relativeX: -150; relativeY: -250 }
+ PathLine { relativeX: 75; relativeY: 200 }
+ PathLine { relativeX: -75; relativeY: 0 }
+ }
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 10
+ cosmeticStroke: true
+ capStyle: ShapePath.FlatCap
+ joinStyle: ShapePath.BevelJoin
+ PathMove { x: 410; y: 800 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: -25; relativeControlY: -25 }
+ PathQuad { relativeX: 0; relativeY: -50;
+ relativeControlX: 50; relativeControlY: -25 }
+ PathCubic { relativeX: 0; relativeY: 300;
+ relativeControl1X: -250; relativeControl1Y: 100
+ relativeControl2X: 250; relativeControl2Y: 200 }
+ PathLine { relativeX: -150; relativeY: -250 }
+ PathLine { relativeX: 75; relativeY: 200 }
+ PathLine { relativeX: -75; relativeY: 0 }
+ }
+ }
+ Shapes {
+ x: 100
+ y: 200
+ scale: 1.25
+ }
+ Shapes {
+ x: 700
+ }
+ Shapes {
+ x: 1100
+ y: 100
+ scale: 0.5
+ }
+ Shapes {
+ x: 1100
+ y: -300
+ scale: 0.25
+ }
+}
diff --git a/tests/manual/painterpathquickshape/CMakeLists.txt b/tests/manual/painterpathquickshape/CMakeLists.txt
index 12b0634e47..242e95938a 100644
--- a/tests/manual/painterpathquickshape/CMakeLists.txt
+++ b/tests/manual/painterpathquickshape/CMakeLists.txt
@@ -59,6 +59,7 @@ set(qml_resource_files
"linearGradient.qml"
"quadraticCurve.qml"
"radialGradient.qml"
+ "regularAndCosmeticStrokes.qml"
"strokeOrFill.qml"
"text.qml"
"tiger.qml"
diff --git a/tests/manual/painterpathquickshape/ControlPanel.qml b/tests/manual/painterpathquickshape/ControlPanel.qml
index ae68682a2b..ebb297e591 100644
--- a/tests/manual/painterpathquickshape/ControlPanel.qml
+++ b/tests/manual/painterpathquickshape/ControlPanel.qml
@@ -13,12 +13,13 @@ Item {
property color backgroundColor: setBackground.checked ? Qt.rgba(bgColor.color.r, bgColor.color.g, bgColor.color.b, 1.0) : Qt.rgba(0,0,0,0)
property color outlineColor: enableOutline.checked ? Qt.rgba(outlineColor.color.r, outlineColor.color.g, outlineColor.color.b, outlineAlpha) : Qt.rgba(0,0,0,0)
- property color fillColor: Qt.rgba(fillColor.color.r, fillColor.color.g, fillColor.color.b, pathAlpha)
+ property color fillColor: enableFill.checked ? Qt.rgba(fillColor.color.r, fillColor.color.g, fillColor.color.b, pathAlpha) : Qt.rgba(0,0,0,0)
property alias pathAlpha: alphaSlider.value
property alias fillRule: fillRule.currentValue
property alias outlineAlpha: outlineAlphaSlider.value
- property real outlineWidth: cosmeticPen.checked ? outlineWidthEdit.text / scale : outlineWidthEdit.text
+ property real outlineWidth: outlineWidthEdit.text
property alias outlineStyle: outlineStyle.currentValue
+ property alias cosmeticPen: cosmeticPen.checked
property alias capStyle: capStyle.currentValue
property alias joinStyle: joinStyle.currentValue
property alias debugCurves: enableDebug.checked
@@ -26,6 +27,7 @@ Item {
property alias painterComparison: painterComparison.currentIndex
property alias painterComparisonColor: painterComparisonColor.color
property alias painterComparisonAlpha: painterComparisonColorAlpha.value
+ property alias fillEnabled: enableFill.checked
property alias outlineEnabled: enableOutline.checked
property alias gradientType: gradientType.currentIndex
property alias fillScaleX: fillTransformSlider.value
@@ -39,6 +41,7 @@ Item {
property real pathMargin: marginEdit.text
Settings {
+ property alias enableFill: enableFill.checked
property alias enableOutline: enableOutline.checked
property alias outlineColor: outlineColor.color
property alias outlineWidth: outlineWidthEdit.text
@@ -222,6 +225,14 @@ Item {
}
}
RowLayout {
+ CheckBox {
+ id: enableFill
+ text: "Enable fill"
+ palette.windowText: "white"
+ Layout.fillWidth: false
+ }
+ RowLayout {
+ opacity: enableFill.checked ? 1 : 0
Label {
text: "Fill color"
color: "white"
@@ -287,6 +298,7 @@ Item {
to: 1.0
value: 1.0
}
+ }
}
RowLayout {
CheckBox {
@@ -376,16 +388,12 @@ Item {
}
TextField {
id: outlineWidthEdit
- text: (cosmeticPen.checked ? outlineWidthSlider.value: outlineWidthSlider.value ** 2).toFixed(2)
+ text: (outlineWidthSlider.value ** 2).toFixed(2)
Layout.fillWidth: false
onEditingFinished: {
let val = +text
- if (val > 0) {
- if (cosmeticPen.checked)
- outlineWidth.value = val * scale
- else
- outlineWidth.value = Math.sqrt(val)
- }
+ if (val >= 0)
+ outlineWidthSlider.value = Math.sqrt(val)
}
}
Slider {
@@ -393,7 +401,7 @@ Item {
Layout.fillWidth: true
from: 0.0
to: 10.0
- value: Math.sqrt(10)
+ Component.onCompleted: outlineWidthSlider.value = Math.sqrt(outlineWidthEdit.text)
}
CheckBox {
id: cosmeticPen
diff --git a/tests/manual/painterpathquickshape/ControlledShape.qml b/tests/manual/painterpathquickshape/ControlledShape.qml
index ea2f7c7236..8ed9c70ea7 100644
--- a/tests/manual/painterpathquickshape/ControlledShape.qml
+++ b/tests/manual/painterpathquickshape/ControlledShape.qml
@@ -90,6 +90,7 @@ Item {
strokeStyle: controlPanel.outlineStyle
joinStyle: controlPanel.joinStyle
capStyle: controlPanel.capStyle
+ cosmeticStroke: controlPanel.cosmeticPen
fillTransform: Qt.matrix4x4(controlPanel.fillScaleX,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1)
}
@@ -114,7 +115,7 @@ Item {
id: debugPaintPath
shape: shapePath
visible: controlPanel.painterComparison > 0
- color: controlPanel.painterComparisonColor
+ color: controlPanel.fillEnabled ? controlPanel.painterComparisonColor : "transparent"
opacity: controlPanel.painterComparisonAlpha
z: controlPanel.painterComparison > 1 ? -1 : 0
pathScale: controlPanel.scale
diff --git a/tests/manual/painterpathquickshape/main.qml b/tests/manual/painterpathquickshape/main.qml
index 4998685758..376598bb61 100644
--- a/tests/manual/painterpathquickshape/main.qml
+++ b/tests/manual/painterpathquickshape/main.qml
@@ -134,6 +134,11 @@ Window {
}
ListElement {
+ text: "Regular and cosmetic strokes"
+ source: "regularAndCosmeticStrokes.qml"
+ }
+
+ ListElement {
text: "Qt! text"
source: "text.qml"
}
diff --git a/tests/manual/painterpathquickshape/regularAndCosmeticStrokes.qml b/tests/manual/painterpathquickshape/regularAndCosmeticStrokes.qml
new file mode 100644
index 0000000000..e2305073f0
--- /dev/null
+++ b/tests/manual/painterpathquickshape/regularAndCosmeticStrokes.qml
@@ -0,0 +1,92 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Shapes
+
+Item {
+ implicitWidth: 920
+ implicitHeight: 240
+ Shape {
+ y: 30
+ transform: Scale { xScale: xscale.value; yScale: yscale.value }
+ preferredRendererType: Shape.CurveRenderer
+ ShapePath {
+ strokeColor: "#ff000000"
+ strokeWidth: 2
+ cosmeticStroke: true
+ capStyle: ShapePath.FlatCap
+ joinStyle: ShapePath.MiterJoin
+ miterLimit: 4
+ fillColor: "#00000000"
+ fillRule: ShapePath.WindingFill
+ PathSvg { id: thePath; path: "M 10 20 L 14 20 L 14 24 L 40 100 Q 120 150 39 200 Z" }
+ }
+ }
+ Text {
+ x: 4
+ anchors { top: toolbar.bottom }
+ text: "cosmetic\n2px, scaled"
+ }
+ Shape {
+ transform: Matrix4x4 { matrix: PlanarTransform.fromAffineMatrix(xscale.value, 0, 0, yscale.value, 100, 30) }
+ preferredRendererType: Shape.CurveRenderer
+ ShapePath {
+ strokeColor: "#ff000000"
+ strokeWidth: 2
+ capStyle: ShapePath.FlatCap
+ joinStyle: ShapePath.MiterJoin
+ miterLimit: 4
+ fillColor: "#00000000"
+ fillRule: ShapePath.WindingFill
+ PathSvg { path: thePath.path }
+ }
+ }
+ Text {
+ x: 120
+ anchors { top: toolbar.bottom }
+ text: "2px, scaled"
+ }
+ Shape {
+ transform: Translate { x: 300; y: 30 }
+ preferredRendererType: Shape.CurveRenderer
+ ShapePath {
+ strokeColor: "#ff000000"
+ strokeWidth: 2
+ capStyle: ShapePath.FlatCap
+ joinStyle: ShapePath.MiterJoin
+ miterLimit: 4
+ fillColor: "#00000000"
+ fillRule: ShapePath.WindingFill
+ PathSvg { path: thePath.path }
+ }
+ }
+ Text {
+ x: 315
+ anchors { top: toolbar.bottom }
+ text: "2px, normal"
+ }
+
+ Row {
+ id: toolbar
+ spacing: 10
+ Slider {
+ id: xscale
+ from: 1
+ to: 10
+ }
+ Text {
+ text: "x scale: " + xscale.value.toFixed(2)
+ }
+
+ Slider {
+ id: yscale
+ from: 1
+ to: 10
+ }
+ Text {
+ text: "y scale: " + yscale.value.toFixed(2)
+ }
+ }
+}
diff --git a/tests/manual/vectorimagetest/data/styling/stroking_cosmetic.svg b/tests/manual/vectorimagetest/data/styling/stroking_cosmetic.svg
new file mode 100644
index 0000000000..c4c3aff264
--- /dev/null
+++ b/tests/manual/vectorimagetest/data/styling/stroking_cosmetic.svg
@@ -0,0 +1,33 @@
+<svg viewBox="0 0 500 240">
+ <!-- cosmetic -->
+ <path
+ vector-effect="non-scaling-stroke"
+ transform="scale(4, 1)"
+ d="M10,20 L40,100 L39,200 z"
+ stroke="black"
+ stroke-width="2px"
+ fill="none"></path>
+
+ <text x="50" y="225">cosmetic 2px, scaled</text>
+
+ <!-- scaled -->
+ <path
+ transform="translate(100,0) scale(4,1)"
+ d="M10,20 L40,100 L39,200 z"
+ stroke="black"
+ stroke-width="2px"
+ fill="none"></path>
+
+ <text x="200" y="225">2px, scaled</text>
+
+ <!-- normal -->
+ <path
+ transform="translate(300, 0)"
+ d="M10,20 L40,100 L39,200 z"
+ stroke="black"
+ stroke-width="2px"
+ fill="none"></path>
+
+ <text x="315" y="225">2px, normal</text>
+
+</svg>