Improvements to curve renderer

This contains numerous updates/improvements to the curve renderer
and the manual test.

Most notably, it replaces the Delaunay triangulator with a more robust
algorithm which uses qTriangulate() for the internal hull (like the basic
code path) and a separate triangulation for curves which adds triangles
on the outside and calculates texture coordinates accordingly. This
gives antialiasing on straight lines as well.

It also has multiple improvements to the cubic-to-quad algorithm, for
instance circles now look as they should.

Currently, the QTriangulatingStroker is used for solid strokes (causing
jagged edges there) and the QPainterPath stroker is used for dash strokes
(causing some issues with antialiasing).

Pick-to: 6.6
Task-number: QTBUG-104122
Change-Id: Ic195aa874dc73c62359a93764ef38a09efb012e3
Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2023-06-20 14:16:41 +02:00 committed by Paul Olav Tvete
parent 2b9aa54610
commit 4a1e5d8e74
15 changed files with 1526 additions and 1841 deletions

View File

@ -21,7 +21,6 @@ qt_internal_add_qml_module(QuickShapesPrivate
qquickshapegenericrenderer.cpp qquickshapegenericrenderer_p.h qquickshapegenericrenderer.cpp qquickshapegenericrenderer_p.h
qquickshapesglobal.h qquickshapesglobal_p.h qquickshapesglobal.h qquickshapesglobal_p.h
qquickshapecurverenderer.cpp qquickshapecurverenderer_p.h qquickshapecurverenderer_p_p.h qquickshapecurverenderer.cpp qquickshapecurverenderer_p.h qquickshapecurverenderer_p_p.h
qt_delaunay_triangulator.cpp
qt_quadratic_bezier.cpp qt_quadratic_bezier.cpp
qquickshapesoftwarerenderer.cpp qquickshapesoftwarerenderer_p.h qquickshapesoftwarerenderer.cpp qquickshapesoftwarerenderer_p.h
PUBLIC_LIBRARIES PUBLIC_LIBRARIES

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,7 @@ public:
static QuadPath fromPainterPath(const QPainterPath &path); static QuadPath fromPainterPath(const QPainterPath &path);
QPainterPath toPainterPath() const; QPainterPath toPainterPath() const;
QuadPath subPathsClosed() const; QuadPath subPathsClosed() const;
QuadPath flattened() const;
class Element class Element
{ {
@ -131,6 +132,8 @@ public:
return QVector2D(-tan.y(), tan.x()); return QVector2D(-tan.y(), tan.x());
} }
float extent() const;
private: private:
int intersectionsAtY(float y, float *fractions) const; int intersectionsAtY(float y, float *fractions) const;
@ -306,6 +309,7 @@ private:
QPainterPath fillPath; QPainterPath fillPath;
QPainterPath originalPath; QPainterPath originalPath;
QuadPath path; QuadPath path;
QuadPath qPath; // TODO: better name
QColor fillColor; QColor fillColor;
Qt::FillRule fillRule = Qt::OddEvenFill; Qt::FillRule fillRule = Qt::OddEvenFill;
QPen pen; QPen pen;
@ -321,8 +325,9 @@ private:
void deleteAndClear(NodeList *nodeList); void deleteAndClear(NodeList *nodeList);
QVector<QSGGeometryNode *> addPathNodesBasic(const PathData &pathData, NodeList *debugNodes); QVector<QSGGeometryNode *> addPathNodesBasic(const PathData &pathData, NodeList *debugNodes);
QVector<QSGGeometryNode *> addPathNodesLineShader(const PathData &pathData, NodeList *debugNodes);
QVector<QSGGeometryNode *> addPathNodesDelaunayTest(const PathData &pathData, NodeList *debugNodes); QVector<QSGGeometryNode *> addStrokeNodes(const PathData &pathData, NodeList *debugNodes);
QVector<QSGGeometryNode *> addNodesStrokeShader(const PathData &pathData, NodeList *debugNodes);
QSGGeometryNode *addLoopBlinnNodes(const QTriangleSet &triangles, QSGGeometryNode *addLoopBlinnNodes(const QTriangleSet &triangles,
const QVarLengthArray<quint32> &extraIndices, const QVarLengthArray<quint32> &extraIndices,

View File

@ -25,52 +25,9 @@ QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcShapeCurveRenderer); Q_DECLARE_LOGGING_CATEGORY(lcShapeCurveRenderer);
struct QtPathVertex
{
QVector2D point;
int id;
quint32 binX = 0;
quint32 binY = 0;
};
struct QtPathEdge
{
quint32 startIndex;
quint32 endIndex;
int id;
};
struct QtPathTriangle
{
QtPathTriangle(quint32 i1, quint32 i2, quint32 i3, int d) : v1Index(i1), v2Index(i2), v3Index(i3), id(d) {}
quint32 v1Index;
quint32 v2Index;
quint32 v3Index;
quint32 adjacentTriangle1 = quint32(-1); // Adjacent to v1-v2
quint32 adjacentTriangle2 = quint32(-1); // Adjacent to v2-v3
quint32 adjacentTriangle3 = quint32(-1); // Adjacent to v3-v1
// Used by triangulator
quint32 lastSeenVertex = quint32(-1);
// Should this triangle be rendered? Set to false for triangles connecting to super-triangle
bool isValid = true;
int id;
};
constexpr bool operator==(const QtPathTriangle& lhs, const QtPathTriangle& rhs)
{
return lhs.id == rhs.id
&& lhs.v1Index == rhs.v1Index
&& lhs.v2Index == rhs.v2Index
&& lhs.v3Index == rhs.v3Index;
}
class QBezier; class QBezier;
Q_QUICKSHAPES_PRIVATE_EXPORT QPolygonF qt_toQuadratics(const QBezier &b, qreal errorLimit = 0.2); Q_QUICKSHAPES_PRIVATE_EXPORT void qt_toQuadratics(const QBezier &b, QPolygonF *out,
Q_QUICKSHAPES_PRIVATE_EXPORT QList<QtPathTriangle> qtDelaunayTriangulator(const QList<QtPathVertex> &vertices, const QList<QtPathEdge> &edges, const QPainterPath &path); qreal errorLimit = 0.01);
QT_END_NAMESPACE QT_END_NAMESPACE

File diff suppressed because it is too large Load Diff

View File

@ -8,38 +8,81 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
#if 0
static bool qt_isQuadratic(const QBezier &b)
{
const qreal f = 3.0 / 2.0;
const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1());
const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4());
return c1 == c2;
}
#endif
static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp) static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp)
{ {
// Construct a cubic from the quadratic, and compare its control points to the originals' static bool init = false;
const QRectF bounds = b.bounds(); const int numSteps = 21;
qreal dim = QLineF(bounds.topLeft(), bounds.bottomRight()).length(); Q_STATIC_ASSERT(numSteps % 2 == 1); // numTries must be odd
if (qFuzzyIsNull(dim)) static qreal t2s[numSteps];
return 0; static qreal tmts[numSteps];
const qreal f = 2.0 / 3; if (!init) {
const QPointF cp1 = b.pt1() + f * (qcp - b.pt1()); // Precompute bezier factors
const QPointF cp2 = b.pt4() + f * (qcp - b.pt4()); qreal t = 0.20;
const QLineF d1(b.pt2(), cp1); const qreal step = (1 - (2 * t)) / (numSteps - 1);
const QLineF d2(b.pt3(), cp2); for (int i = 0; i < numSteps; i++) {
return qMax(d1.length(), d2.length()) / dim; t2s[i] = t * t;
tmts[i] = 2 * t * (1 - t);
t += step;
}
init = true;
}
const QPointF midPoint = b.midPoint();
auto distForIndex = [&](int i) -> qreal {
QPointF qp = (t2s[numSteps - 1 - i] * b.pt1()) + (tmts[i] * qcp) + (t2s[i] * b.pt4());
QPointF d = midPoint - qp;
return QPointF::dotProduct(d, d);
};
const int halfSteps = (numSteps - 1) / 2;
bool foundIt = false;
const qreal centerDist = distForIndex(halfSteps);
qreal minDist = centerDist;
// Search for the minimum in right half
for (int i = 0; i < halfSteps; i++) {
qreal tDist = distForIndex(halfSteps + 1 + i);
if (tDist < minDist) {
minDist = tDist;
} else {
foundIt = (i > 0);
break;
}
}
if (!foundIt) {
// Search in left half
minDist = centerDist;
for (int i = 0; i < halfSteps; i++) {
qreal tDist = distForIndex(halfSteps - 1 - i);
if (tDist < minDist) {
minDist = tDist;
} else {
foundIt = (i > 0);
break;
}
}
}
return foundIt ? minDist : centerDist;
} }
static qreal qt_quadraticForCubic(const QBezier &b, QPointF *qcp) static QPointF qt_quadraticForCubic(const QBezier &b)
{ {
const QLineF st = b.startTangent(); const QLineF st = b.startTangent();
const QLineF et = b.endTangent(); const QLineF et = b.endTangent();
if (st.intersects(et, qcp) == QLineF::NoIntersection) const QPointF midPoint = b.midPoint();
*qcp = b.midPoint(); bool valid = true;
return qt_scoreQuadratic(b, *qcp); QPointF quadControlPoint;
if (st.intersects(et, &quadControlPoint) == QLineF::NoIntersection) {
valid = false;
} else {
// Check if intersection is on wrong side
const QPointF bl = b.pt4() - b.pt1();
const QPointF ml = midPoint - b.pt1();
const QPointF ql = quadControlPoint - b.pt1();
qreal cx1 = (ml.x() * bl.y()) - (ml.y() * bl.x());
qreal cx2 = (ql.x() * bl.y()) - (ql.y() * bl.x());
valid = (std::signbit(cx1) == std::signbit(cx2));
}
return valid ? quadControlPoint : midPoint;
} }
static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints) static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
@ -57,22 +100,34 @@ static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
const QBezier n = orig.mapBy(xf); const QBezier n = orig.mapBy(xf);
Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().y()))); Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().y())));
const qreal p = n.pt3().x() * n.pt2().y(); const qreal x2 = n.pt2().x();
const qreal q = n.pt4().x() * n.pt2().y(); const qreal x3 = n.pt3().x();
const qreal r = n.pt2().x() * n.pt3().y(); const qreal x4 = n.pt4().x();
const qreal s = n.pt4().x() * n.pt3().y(); const qreal y2 = n.pt2().y();
const qreal y3 = n.pt3().y();
const qreal a = 36 * ((-3 * p) + (2 * q) + (3 * r) - s); const qreal p = x3 * y2;
if (!a) const qreal q = x4 * y2;
return 0; const qreal r = x2 * y3;
const qreal b = -18 * (((3 * p) - q) - (3 * r)); const qreal s = x4 * y3;
const qreal a = 18 * ((-3 * p) + (2 * q) + (3 * r) - s);
if (qFuzzyIsNull(float(a))) {
if (std::signbit(y2) != std::signbit(y3) && qFuzzyCompare(float(x4 - x3), float(x2))) {
tpoints[0] = 0.5; // approx
return 1;
} else if (!a) {
return 0;
}
}
const qreal b = 18 * (((3 * p) - q) - (3 * r));
const qreal c = 18 * (r - p); const qreal c = 18 * (r - p);
const qreal rad = (b * b) - (2 * a * c); const qreal rad = (b * b) - (4 * a * c);
if (rad < 0) if (rad < 0)
return 0; return 0;
const qreal sqr = qSqrt(rad); const qreal sqr = qSqrt(rad);
const qreal root1 = (b + sqr) / a; const qreal root1 = (-b + sqr) / (2 * a);
const qreal root2 = (b - sqr) / a; const qreal root2 = (-b - sqr) / (2 * a);
int res = 0; int res = 0;
if (isValidRoot(root1)) if (isValidRoot(root1))
@ -86,54 +141,53 @@ static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
return res; return res;
} }
static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, qreal spanlength, qreal errorLimit) static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff)
{ {
Q_ASSERT((spanlength > 0) && !(spanlength > 1)); QPointF qcp = qt_quadraticForCubic(b);
if (maxSplits <= 0 || qt_scoreQuadratic(b, qcp) < maxDiff) {
QPointF qcp;
bool isOk = (qt_quadraticForCubic(b, &qcp) < errorLimit); // error limit, careful
if (isOk || spanlength < 0.1) {
p->append(qcp); p->append(qcp);
p->append(b.pt4()); p->append(b.pt4());
} else { } else {
QBezier rhs = b; QBezier rhs = b;
QBezier lhs; QBezier lhs;
rhs.parameterSplitLeft(0.5, &lhs); rhs.parameterSplitLeft(0.5, &lhs);
qt_addToQuadratics(lhs, p, spanlength / 2, errorLimit); qt_addToQuadratics(lhs, p, maxSplits - 1, maxDiff);
qt_addToQuadratics(rhs, p, spanlength / 2, errorLimit); qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff);
} }
} }
QPolygonF qt_toQuadratics(const QBezier &b, qreal errorLimit) void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit)
{ {
out->resize(0);
out->append(b.pt1());
{
// Shortcut if the cubic is really a quadratic
const qreal f = 3.0 / 2.0;
const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1());
const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4());
if (c1 == c2) {
out->append(c1);
out->append(b.pt4());
return;
}
}
QPolygonF res;
res.reserve(16);
res.append(b.pt1());
const QRectF cpr = b.bounds(); const QRectF cpr = b.bounds();
qreal epsilon = QLineF(cpr.topLeft(), cpr.bottomRight()).length() * 0.5 * errorLimit; const QPointF dim = cpr.bottomRight() - cpr.topLeft();
bool degenerate = false; qreal maxDiff = QPointF::dotProduct(dim, dim) * errorLimit * errorLimit; // maxdistance^2
if (QLineF(b.pt2(), b.pt1()).length() < epsilon) {
res.append(b.pt3());
degenerate = true;
} else if (QLineF(b.pt4(), b.pt3()).length() < epsilon) {
res.append(b.pt2());
degenerate = true;
}
if (degenerate) {
res.append(b.pt4());
return res;
}
qreal infPoints[2]; qreal infPoints[2];
int numInfPoints = qt_getInflectionPoints(b, infPoints); int numInfPoints = qt_getInflectionPoints(b, infPoints);
const int maxSubSplits = numInfPoints > 0 ? 2 : 3;
qreal t0 = 0; qreal t0 = 0;
for (int i = 0; i < numInfPoints + 1; i++) { // #segments == #inflectionpoints + 1 // number of main segments == #inflectionpoints + 1
for (int i = 0; i < numInfPoints + 1; i++) {
qreal t1 = (i < numInfPoints) ? infPoints[i] : 1; qreal t1 = (i < numInfPoints) ? infPoints[i] : 1;
QBezier segment = b.bezierOnInterval(t0, t1); QBezier segment = b.bezierOnInterval(t0, t1);
qt_addToQuadratics(segment, &res, t1 - t0, errorLimit); qt_addToQuadratics(segment, out, maxSubSplits, maxDiff);
t0 = t1; t0 = t1;
} }
return res;
} }
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -14,7 +14,6 @@ qt_internal_add_manual_test(painterPathQuickShape
Qt::Quick Qt::Quick
Qt::QuickPrivate Qt::QuickPrivate
Qt::QuickShapesPrivate Qt::QuickShapesPrivate
Qt::SvgPrivate
) )
@ -29,6 +28,7 @@ set(qml_resource_files
"SmallPolygon.qml" "SmallPolygon.qml"
"Squircle.qml" "Squircle.qml"
"ControlledShape.qml" "ControlledShape.qml"
"Mussel.qml"
"Graziano.ttf" "Graziano.ttf"
"CubicShape.qml" "CubicShape.qml"
"hand-print.svg" "hand-print.svg"

View File

@ -9,10 +9,13 @@ import QtQuick.Dialogs
Item { Item {
property real scale: +scaleSlider.value.toFixed(4) property real scale: +scaleSlider.value.toFixed(4)
property color outlineColor: enableOutline.checked ? Qt.rgba(outlineColor.color.r, outlineColor.color.g, outlineColor.color.b, pathAlpha) : Qt.rgba(0,0,0,0) 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: Qt.rgba(fillColor.color.r, fillColor.color.g, fillColor.color.b, pathAlpha)
property alias pathAlpha: alphaSlider.value property alias pathAlpha: alphaSlider.value
property alias outlineWidth: outlineWidth.value property alias outlineAlpha: outlineAlphaSlider.value
property real outlineWidth: cosmeticPen.checked ? outlineWidth.value / scale : outlineWidth.value ** 2
property alias outlineStyle: outlineStyle.currentValue property alias outlineStyle: outlineStyle.currentValue
property alias capStyle: capStyle.currentValue property alias capStyle: capStyle.currentValue
property alias joinStyle: joinStyle.currentValue property alias joinStyle: joinStyle.currentValue
@ -27,6 +30,7 @@ Item {
property alias preferCurve: rendererLabel.preferCurve property alias preferCurve: rendererLabel.preferCurve
property int subShape: pickSubShape.checked ? subShapeSelector.value : -1 property int subShape: pickSubShape.checked ? subShapeSelector.value : -1
property bool subShapeGreaterThan : pickSubShapeGreaterThan.checked
property real pathMargin: marginEdit.text property real pathMargin: marginEdit.text
@ -100,10 +104,10 @@ Item {
text: "Alpha" text: "Alpha"
color: "white" color: "white"
} }
CheckBox { id: pickSubShape } CheckBox {
Label {
text: "Pick SVG sub-shape" text: "Pick SVG sub-shape"
color: "white" id: pickSubShape
palette.windowText: "white"
} }
SpinBox { SpinBox {
id: subShapeSelector id: subShapeSelector
@ -112,6 +116,35 @@ Item {
to: 999 to: 999
editable: true editable: true
} }
CheckBox {
id: pickSubShapeGreaterThan
visible: pickSubShape.checked
text: "show greater than"
palette.windowText: "white"
}
CheckBox {
id: setBackground
text: "Solid background"
palette.windowText: "white"
}
RowLayout {
visible: setBackground.checked
Rectangle {
id: bgColor
property color selectedColor: "#a9a9a9"
color: selectedColor
border.color: "black"
border.width: 2
width: 21
height: 21
MouseArea {
anchors.fill: parent
onClicked: {
bgColorDialog.open()
}
}
}
}
} }
RowLayout { RowLayout {
Label { Label {
@ -256,7 +289,6 @@ Item {
} }
} }
} }
Label { Label {
text: "Outline width" text: "Outline width"
color: "white" color: "white"
@ -265,13 +297,34 @@ Item {
id: outlineWidth id: outlineWidth
Layout.fillWidth: true Layout.fillWidth: true
from: 0.0 from: 0.0
to: 100.0 to: 10.0
value: 10.0 value: Math.sqrt(10)
}
CheckBox {
id: cosmeticPen
text: "Cosmetic pen"
palette.windowText: "white"
}
Label {
text: "Outline alpha"
color: "white"
}
Slider {
id: outlineAlphaSlider
Layout.fillWidth: true
from: 0.0
to: 1.0
value: 1.0
} }
} }
} }
} }
ColorDialog {
id: bgColorDialog
selectedColor: bgColor.selectedColor
onAccepted: bgColor.selectedColor = selectedColor
}
ColorDialog { ColorDialog {
id: outlineColorDialog id: outlineColorDialog
selectedColor: outlineColor.selectedColor selectedColor: outlineColor.selectedColor

View File

@ -19,16 +19,17 @@ Rectangle {
property point pt: Qt.point(cx, cy) property point pt: Qt.point(cx, cy)
DragHandler { DragHandler {
id: handler
xAxis.minimum: -controlPanel.pathMargin xAxis.minimum: -controlPanel.pathMargin
yAxis.minimum: -controlPanel.pathMargin yAxis.minimum: -controlPanel.pathMargin
} xAxis.onActiveValueChanged: {
onXChanged: { cx = (x + width/2) / controlPanel.scale
cx = (x + width/2) / controlPanel.scale controlPanel.updatePath()
controlPanel.updatePath() }
} yAxis.onActiveValueChanged: {
onYChanged: { cy = (y + height/2) / controlPanel.scale
cy = (y + height/2) / controlPanel.scale controlPanel.updatePath()
controlPanel.updatePath() }
} }
Component.onCompleted: { Component.onCompleted: {
@ -43,5 +44,4 @@ Rectangle {
y = cy * controlPanel.scale - height/2 y = cy * controlPanel.scale - height/2
} }
} }
} }

View File

@ -13,6 +13,7 @@ Item {
property alias strokeColor: shapePath.strokeColor property alias strokeColor: shapePath.strokeColor
property alias strokeWidth: shapePath.strokeWidth property alias strokeWidth: shapePath.strokeWidth
property alias fillRule: shapePath.fillRule property alias fillRule: shapePath.fillRule
property alias shapeTransform: shape.transform
property alias startX: shapePath.startX property alias startX: shapePath.startX
property alias startY: shapePath.startY property alias startY: shapePath.startY
@ -58,48 +59,48 @@ Item {
property var gradients: [ null, linearGradient, radialGradient, conicalGradient ] property var gradients: [ null, linearGradient, radialGradient, conicalGradient ]
Shape { Item {
id: shape
x: 0
y: 0
preferredRendererType: controlPanel.preferCurve ? Shape.CurveRenderer : Shape.UnknownRenderer
onRendererTypeChanged: {
controlPanel.rendererName = rendererType == Shape.SoftwareRenderer ? "Software" :
rendererType == Shape.GeometryRenderer ? "Geometry" :
rendererType == Shape.CurveRenderer ? "Curve" : "Unknown";
}
transform: [ transform: [
Scale { Scale {
xScale: controlPanel.scale xScale: controlPanel.scale
yScale: controlPanel.scale yScale: controlPanel.scale
origin.x: shape.implicitWidth / 2 origin.x: shape.implicitWidth / 2
origin.y: shape.implicitHeight / 2 origin.y: shape.implicitHeight / 2
} }
] ]
Shape {
id: shape
x: 0
y: 0
preferredRendererType: controlPanel.preferCurve ? Shape.CurveRenderer : Shape.UnknownRenderer
onRendererTypeChanged: {
controlPanel.rendererName = rendererType == Shape.SoftwareRenderer ? "Software" :
rendererType == Shape.GeometryRenderer ? "Geometry" :
rendererType == Shape.CurveRenderer ? "Curve" : "Unknown";
}
ShapePath { ShapePath {
id: shapePath id: shapePath
fillRule: ShapePath.WindingFill fillRule: ShapePath.WindingFill
fillGradient: gradients[controlPanel.gradientType] fillGradient: gradients[controlPanel.gradientType]
strokeColor: controlPanel.outlineColor strokeColor: controlPanel.outlineColor
fillColor: controlPanel.fillColor fillColor: controlPanel.fillColor
strokeWidth: controlPanel.outlineWidth strokeWidth: controlPanel.outlineWidth
strokeStyle: controlPanel.outlineStyle strokeStyle: controlPanel.outlineStyle
joinStyle: controlPanel.joinStyle joinStyle: controlPanel.joinStyle
capStyle: controlPanel.capStyle capStyle: controlPanel.capStyle
} }
Repeater { Repeater {
model: topLevel.delegate model: topLevel.delegate
onModelChanged: { onModelChanged: {
shapePath.pathElements = [] shapePath.pathElements = []
for (var i = 0; i < model.length; ++i) for (var i = 0; i < model.length; ++i)
shapePath.pathElements.push(model[i]) shapePath.pathElements.push(model[i])
}
} }
} }
} }
Connections { Connections {
target: controlPanel target: controlPanel
function onPathChanged() { function onPathChanged() {

View File

@ -0,0 +1,42 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Shapes
ControlledShape {
delegate: [
PathMove { x: p1.cx; y: p1.cy },
PathQuad { x: p2.cx; y: p2.cy; controlX: c1.cx; controlY: c1.cy },
PathQuad { x: p3.cx; y: p3.cy; controlX: c2.cx; controlY: c2.cy },
PathLine { x: p1.cx; y: p1.cy }
]
ControlPoint {
id: p1
cx: 200
cy: 200
}
ControlPoint {
id: c1
color: "blue"
cx: 600
cy: 0
}
ControlPoint {
id: p2
cx: 1000
cy: 200
}
ControlPoint {
id: c2
color: "blue"
cx: 1000
cy: 1000
}
ControlPoint {
id: p3
cx: 200
cy: 1000
}
}

View File

@ -40,19 +40,24 @@ Item {
children = [] children = []
let first = true let first = true
let pickOne = controlPanel.subShape let pickShape = controlPanel.subShape
if (pickOne < 0) let pickOne = pickShape >= 0 && !controlPanel.subShapeGreaterThan
console.debug("Creating " + pathLoader.paths.length + " SVG items") let pickGreater = pickShape >= 0 && controlPanel.subShapeGreaterThan
if (pickOne)
console.log("Creating SVG item", pickShape, "out of", pathLoader.paths.length)
else if (pickGreater)
console.debug("Creating " + (pathLoader.paths.length - pickShape) + " SVG items")
else else
console.log("Creating SVG item", pickOne, "out of", pathLoader.paths.length) console.debug("Creating " + pathLoader.paths.length + " SVG items")
for (var i = 0; i < pathLoader.paths.length; ++i) { for (var i = 0; i < pathLoader.paths.length; ++i) {
if (pickOne >= 0 && pickOne !== i) if ((pickOne && pickShape !== i ) || (pickGreater && i < pickShape))
continue continue
var s = pathLoader.paths[i] var s = pathLoader.paths[i]
var fillColor = pathLoader.fillColors[i] var fillColor = pathLoader.fillColors[i]
let strokeText = ""; let strokeText = "";
let strokeColor = pathLoader.strokeColors[i] let strokeColor = pathLoader.strokeColors[i]
let strokeWidth = pathLoader.strokeWidths[i] let strokeWidth = pathLoader.strokeWidths[i]
let transform = pathLoader.transforms[i]
if (strokeColor) { if (strokeColor) {
if (!strokeWidth) if (!strokeWidth)
strokeWidth = "1.0" // default value defined by SVG standard strokeWidth = "1.0" // default value defined by SVG standard
@ -62,10 +67,11 @@ Item {
fillColor = "#00000000" fillColor = "#00000000"
} }
var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { " var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { \n"
+ "fillColor: \"" + fillColor + "\";" + "fillColor: \"" + fillColor + "\";\n"
+ strokeText + "shapeTransform: Matrix4x4 { matrix: Qt.matrix4x4(" + transform + "); }\n"
+ "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }", + strokeText + "\n"
+ "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }\n",
topLevel, "SvgPathComponent_" + i) topLevel, "SvgPathComponent_" + i)

View File

@ -41,6 +41,10 @@ Window {
text: "CubicShape" text: "CubicShape"
source: "CubicShape.qml" source: "CubicShape.qml"
} }
ListElement {
text: "Mussel"
source: "Mussel.qml"
}
ListElement { ListElement {
text: "Arc Direction" text: "Arc Direction"
source: "arcDirection.qml" source: "arcDirection.qml"
@ -138,6 +142,11 @@ Window {
source: "qrc:/background.png" source: "qrc:/background.png"
smooth: true smooth: true
} }
Rectangle {
id: solidBackground
anchors.fill: flickable
color: controlPanel.backgroundColor
}
Flickable { Flickable {
id: flickable id: flickable
@ -152,10 +161,9 @@ Window {
WheelHandler { WheelHandler {
onWheel: (event)=> { onWheel: (event)=> {
let scale = controlPanel.scale let scale = controlPanel.scale
let posX = event.x // position in scaled path:
let posY = event.y let posX = event.x - controlPanel.pathMargin
let xOff = posX - flickable.contentX let posY = event.y - controlPanel.pathMargin
let yOff = posY - flickable.contentY
let pathX = posX / scale let pathX = posX / scale
let pathY = posY / scale let pathY = posY / scale
@ -166,8 +174,12 @@ Window {
scale = scale / 1.1 scale = scale / 1.1
controlPanel.setScale(scale) controlPanel.setScale(scale)
flickable.contentX = pathX * controlPanel.scale - xOff scale = controlPanel.scale
flickable.contentY = pathY * controlPanel.scale - yOff let scaledPosX = pathX * scale
let scaledPosY = pathY * scale
flickable.contentX += scaledPosX - posX
flickable.contentY += scaledPosY - posY
flickable.returnToBounds() flickable.returnToBounds()
} }
} }

View File

@ -5,8 +5,11 @@
#include <QFile> #include <QFile>
#include <QPainterPath> #include <QPainterPath>
#include <QtSvg/private/qsvgtinydocument_p.h> #include <QXmlStreamReader>
#include <QtSvg/private/qsvggraphics_p.h> #include <QXmlStreamAttributes>
#include <QStack>
#include <QLocale>
#include <QMatrix4x4>
SvgPathLoader::SvgPathLoader() SvgPathLoader::SvgPathLoader()
{ {
@ -20,6 +23,7 @@ struct SvgState
QString fillColor = {}; QString fillColor = {};
QString strokeColor = {}; QString strokeColor = {};
QString strokeWidth = {}; QString strokeWidth = {};
QMatrix4x4 transform;
}; };
void SvgPathLoader::loadPaths() void SvgPathLoader::loadPaths()
@ -28,6 +32,7 @@ void SvgPathLoader::loadPaths()
m_fillColors.clear(); m_fillColors.clear();
m_strokeColors.clear(); m_strokeColors.clear();
m_strokeWidths.clear(); m_strokeWidths.clear();
m_transforms.clear();
if (m_source.isEmpty()) if (m_source.isEmpty())
return; return;
@ -49,8 +54,50 @@ void SvgPathLoader::loadPaths()
QXmlStreamAttributes attrs = reader.attributes(); QXmlStreamAttributes attrs = reader.attributes();
if (reader.isStartElement()) if (reader.isStartElement())
states.push(currentState); states.push(currentState);
if (attrs.hasAttribute(QStringLiteral("transform"))) {
QString t = attrs.value(QStringLiteral("transform")).toString();
const bool isTranslate = t.startsWith(QStringLiteral("translate"));
const bool isScale = t.startsWith(QStringLiteral("scale"));
const bool isMatrix = t.startsWith(QStringLiteral("matrix"));
if (isTranslate || isScale || isMatrix) {
int pStart = t.indexOf(QLatin1Char('('));
int pEnd = t.indexOf(QLatin1Char(')'));
if (pStart >= 0 && pEnd > pStart + 1) {
t = t.mid(pStart + 1, pEnd - pStart - 1);
QStringList coords = t.split(QLatin1Char(','));
if (isMatrix && coords.size() == 6) {
QMatrix3x3 m;
m(0, 0) = coords.at(0).toDouble();
m(1, 0) = coords.at(1).toDouble();
m(2, 0) = 0.0f;
m(0, 1) = coords.at(2).toDouble();
m(1, 1) = coords.at(3).toDouble();
m(2, 1) = 0.0f;
m(0, 2) = coords.at(4).toDouble();
m(1, 2) = coords.at(5).toDouble();
m(2, 2) = 1.0f;
currentState.transform *= QMatrix4x4(m);
} else if (coords.size() == 2) {
qreal c1 = coords.first().toDouble();
qreal c2 = coords.last().toDouble();
if (isTranslate)
currentState.transform.translate(c1, c2);
else if (isScale)
currentState.transform.scale(c1, c2);
}
}
}
}
if (attrs.hasAttribute(QStringLiteral("fill"))) { if (attrs.hasAttribute(QStringLiteral("fill"))) {
currentState.fillColor = attrs.value(QStringLiteral("fill")).toString(); currentState.fillColor = attrs.value(QStringLiteral("fill")).toString();
if (!currentState.fillColor.startsWith("#"))
currentState.fillColor = "";
} else if (attrs.hasAttribute(QStringLiteral("style"))) { } else if (attrs.hasAttribute(QStringLiteral("style"))) {
QString s = attrs.value(QStringLiteral("style")).toString(); QString s = attrs.value(QStringLiteral("style")).toString();
int idx = s.indexOf(QStringLiteral("fill:")); int idx = s.indexOf(QStringLiteral("fill:"));
@ -73,6 +120,22 @@ void SvgPathLoader::loadPaths()
m_fillColors.append(currentState.fillColor); m_fillColors.append(currentState.fillColor);
m_strokeColors.append(currentState.strokeColor); m_strokeColors.append(currentState.strokeColor);
m_strokeWidths.append(currentState.strokeWidth); m_strokeWidths.append(currentState.strokeWidth);
QString t;
for (int i = 0; i < 4; ++i) {
if (i > 0)
t += QLatin1Char(',');
QVector4D row = currentState.transform.row(i);
QLocale c(QLocale::C);
t += QStringLiteral("%1, %2, %3, %4")
.arg(c.toString(row.x()))
.arg(c.toString(row.y()))
.arg(c.toString(row.z()))
.arg(c.toString(row.w()));
}
m_transforms.append(t);
if (attrs.hasAttribute(QStringLiteral("d"))) { if (attrs.hasAttribute(QStringLiteral("d"))) {
m_paths.append(attrs.value(QStringLiteral("d")).toString()); m_paths.append(attrs.value(QStringLiteral("d")).toString());
} }

View File

@ -17,6 +17,7 @@ class SvgPathLoader : public QObject
Q_PROPERTY(QStringList fillColors READ fillColors NOTIFY pathsChanged) Q_PROPERTY(QStringList fillColors READ fillColors NOTIFY pathsChanged)
Q_PROPERTY(QStringList strokeColors READ strokeColors NOTIFY pathsChanged) Q_PROPERTY(QStringList strokeColors READ strokeColors NOTIFY pathsChanged)
Q_PROPERTY(QStringList strokeWidths READ strokeWidths NOTIFY pathsChanged) Q_PROPERTY(QStringList strokeWidths READ strokeWidths NOTIFY pathsChanged)
Q_PROPERTY(QStringList transforms READ transforms NOTIFY pathsChanged)
public: public:
SvgPathLoader(); SvgPathLoader();
@ -53,6 +54,11 @@ public:
return m_strokeWidths; return m_strokeWidths;
} }
QStringList transforms() const
{
return m_transforms;
}
private slots: private slots:
void loadPaths(); void loadPaths();
@ -66,6 +72,7 @@ private:
QStringList m_fillColors; QStringList m_fillColors;
QStringList m_strokeColors; QStringList m_strokeColors;
QStringList m_strokeWidths; QStringList m_strokeWidths;
QStringList m_transforms;
}; };
#endif // SVGPATHLOADER_H #endif // SVGPATHLOADER_H