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

View File

@ -25,52 +25,9 @@ QT_BEGIN_NAMESPACE
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;
Q_QUICKSHAPES_PRIVATE_EXPORT QPolygonF qt_toQuadratics(const QBezier &b, qreal errorLimit = 0.2);
Q_QUICKSHAPES_PRIVATE_EXPORT QList<QtPathTriangle> qtDelaunayTriangulator(const QList<QtPathVertex> &vertices, const QList<QtPathEdge> &edges, const QPainterPath &path);
Q_QUICKSHAPES_PRIVATE_EXPORT void qt_toQuadratics(const QBezier &b, QPolygonF *out,
qreal errorLimit = 0.01);
QT_END_NAMESPACE

File diff suppressed because it is too large Load Diff

View File

@ -8,38 +8,81 @@
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)
{
// Construct a cubic from the quadratic, and compare its control points to the originals'
const QRectF bounds = b.bounds();
qreal dim = QLineF(bounds.topLeft(), bounds.bottomRight()).length();
if (qFuzzyIsNull(dim))
return 0;
const qreal f = 2.0 / 3;
const QPointF cp1 = b.pt1() + f * (qcp - b.pt1());
const QPointF cp2 = b.pt4() + f * (qcp - b.pt4());
const QLineF d1(b.pt2(), cp1);
const QLineF d2(b.pt3(), cp2);
return qMax(d1.length(), d2.length()) / dim;
static bool init = false;
const int numSteps = 21;
Q_STATIC_ASSERT(numSteps % 2 == 1); // numTries must be odd
static qreal t2s[numSteps];
static qreal tmts[numSteps];
if (!init) {
// Precompute bezier factors
qreal t = 0.20;
const qreal step = (1 - (2 * t)) / (numSteps - 1);
for (int i = 0; i < numSteps; i++) {
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 et = b.endTangent();
if (st.intersects(et, qcp) == QLineF::NoIntersection)
*qcp = b.midPoint();
return qt_scoreQuadratic(b, *qcp);
const QPointF midPoint = b.midPoint();
bool valid = true;
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)
@ -57,22 +100,34 @@ static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
const QBezier n = orig.mapBy(xf);
Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().y())));
const qreal p = n.pt3().x() * n.pt2().y();
const qreal q = n.pt4().x() * n.pt2().y();
const qreal r = n.pt2().x() * n.pt3().y();
const qreal s = n.pt4().x() * n.pt3().y();
const qreal x2 = n.pt2().x();
const qreal x3 = n.pt3().x();
const qreal x4 = n.pt4().x();
const qreal y2 = n.pt2().y();
const qreal y3 = n.pt3().y();
const qreal a = 36 * ((-3 * p) + (2 * q) + (3 * r) - s);
if (!a)
return 0;
const qreal b = -18 * (((3 * p) - q) - (3 * r));
const qreal p = x3 * y2;
const qreal q = x4 * y2;
const qreal r = x2 * y3;
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 rad = (b * b) - (2 * a * c);
const qreal rad = (b * b) - (4 * a * c);
if (rad < 0)
return 0;
const qreal sqr = qSqrt(rad);
const qreal root1 = (b + sqr) / a;
const qreal root2 = (b - sqr) / a;
const qreal root1 = (-b + sqr) / (2 * a);
const qreal root2 = (-b - sqr) / (2 * a);
int res = 0;
if (isValidRoot(root1))
@ -86,54 +141,53 @@ static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
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;
bool isOk = (qt_quadraticForCubic(b, &qcp) < errorLimit); // error limit, careful
if (isOk || spanlength < 0.1) {
QPointF qcp = qt_quadraticForCubic(b);
if (maxSplits <= 0 || qt_scoreQuadratic(b, qcp) < maxDiff) {
p->append(qcp);
p->append(b.pt4());
} else {
QBezier rhs = b;
QBezier lhs;
rhs.parameterSplitLeft(0.5, &lhs);
qt_addToQuadratics(lhs, p, spanlength / 2, errorLimit);
qt_addToQuadratics(rhs, p, spanlength / 2, errorLimit);
qt_addToQuadratics(lhs, p, maxSplits - 1, maxDiff);
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();
qreal epsilon = QLineF(cpr.topLeft(), cpr.bottomRight()).length() * 0.5 * errorLimit;
bool degenerate = false;
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;
}
const QPointF dim = cpr.bottomRight() - cpr.topLeft();
qreal maxDiff = QPointF::dotProduct(dim, dim) * errorLimit * errorLimit; // maxdistance^2
qreal infPoints[2];
int numInfPoints = qt_getInflectionPoints(b, infPoints);
const int maxSubSplits = numInfPoints > 0 ? 2 : 3;
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;
QBezier segment = b.bezierOnInterval(t0, t1);
qt_addToQuadratics(segment, &res, t1 - t0, errorLimit);
qt_addToQuadratics(segment, out, maxSubSplits, maxDiff);
t0 = t1;
}
return res;
}
QT_END_NAMESPACE

View File

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

View File

@ -9,10 +9,13 @@ import QtQuick.Dialogs
Item {
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 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 capStyle: capStyle.currentValue
property alias joinStyle: joinStyle.currentValue
@ -27,6 +30,7 @@ Item {
property alias preferCurve: rendererLabel.preferCurve
property int subShape: pickSubShape.checked ? subShapeSelector.value : -1
property bool subShapeGreaterThan : pickSubShapeGreaterThan.checked
property real pathMargin: marginEdit.text
@ -100,10 +104,10 @@ Item {
text: "Alpha"
color: "white"
}
CheckBox { id: pickSubShape }
Label {
CheckBox {
text: "Pick SVG sub-shape"
color: "white"
id: pickSubShape
palette.windowText: "white"
}
SpinBox {
id: subShapeSelector
@ -112,6 +116,35 @@ Item {
to: 999
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 {
Label {
@ -256,7 +289,6 @@ Item {
}
}
}
Label {
text: "Outline width"
color: "white"
@ -265,13 +297,34 @@ Item {
id: outlineWidth
Layout.fillWidth: true
from: 0.0
to: 100.0
value: 10.0
to: 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 {
id: outlineColorDialog
selectedColor: outlineColor.selectedColor

View File

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

View File

@ -13,6 +13,7 @@ Item {
property alias strokeColor: shapePath.strokeColor
property alias strokeWidth: shapePath.strokeWidth
property alias fillRule: shapePath.fillRule
property alias shapeTransform: shape.transform
property alias startX: shapePath.startX
property alias startY: shapePath.startY
@ -58,48 +59,48 @@ Item {
property var gradients: [ null, linearGradient, radialGradient, conicalGradient ]
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";
}
Item {
transform: [
Scale {
xScale: controlPanel.scale
yScale: controlPanel.scale
origin.x: shape.implicitWidth / 2
origin.y: shape.implicitHeight / 2
}
]
xScale: controlPanel.scale
yScale: controlPanel.scale
origin.x: shape.implicitWidth / 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 {
id: shapePath
fillRule: ShapePath.WindingFill
fillGradient: gradients[controlPanel.gradientType]
strokeColor: controlPanel.outlineColor
fillColor: controlPanel.fillColor
strokeWidth: controlPanel.outlineWidth
strokeStyle: controlPanel.outlineStyle
joinStyle: controlPanel.joinStyle
capStyle: controlPanel.capStyle
}
ShapePath {
id: shapePath
fillRule: ShapePath.WindingFill
fillGradient: gradients[controlPanel.gradientType]
strokeColor: controlPanel.outlineColor
fillColor: controlPanel.fillColor
strokeWidth: controlPanel.outlineWidth
strokeStyle: controlPanel.outlineStyle
joinStyle: controlPanel.joinStyle
capStyle: controlPanel.capStyle
}
Repeater {
model: topLevel.delegate
onModelChanged: {
shapePath.pathElements = []
for (var i = 0; i < model.length; ++i)
shapePath.pathElements.push(model[i])
Repeater {
model: topLevel.delegate
onModelChanged: {
shapePath.pathElements = []
for (var i = 0; i < model.length; ++i)
shapePath.pathElements.push(model[i])
}
}
}
}
Connections {
target: controlPanel
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 = []
let first = true
let pickOne = controlPanel.subShape
if (pickOne < 0)
console.debug("Creating " + pathLoader.paths.length + " SVG items")
let pickShape = controlPanel.subShape
let pickOne = pickShape >= 0 && !controlPanel.subShapeGreaterThan
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
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) {
if (pickOne >= 0 && pickOne !== i)
if ((pickOne && pickShape !== i ) || (pickGreater && i < pickShape))
continue
var s = pathLoader.paths[i]
var fillColor = pathLoader.fillColors[i]
let strokeText = "";
let strokeColor = pathLoader.strokeColors[i]
let strokeWidth = pathLoader.strokeWidths[i]
let transform = pathLoader.transforms[i]
if (strokeColor) {
if (!strokeWidth)
strokeWidth = "1.0" // default value defined by SVG standard
@ -62,10 +67,11 @@ Item {
fillColor = "#00000000"
}
var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { "
+ "fillColor: \"" + fillColor + "\";"
+ strokeText
+ "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }",
var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { \n"
+ "fillColor: \"" + fillColor + "\";\n"
+ "shapeTransform: Matrix4x4 { matrix: Qt.matrix4x4(" + transform + "); }\n"
+ strokeText + "\n"
+ "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }\n",
topLevel, "SvgPathComponent_" + i)

View File

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

View File

@ -5,8 +5,11 @@
#include <QFile>
#include <QPainterPath>
#include <QtSvg/private/qsvgtinydocument_p.h>
#include <QtSvg/private/qsvggraphics_p.h>
#include <QXmlStreamReader>
#include <QXmlStreamAttributes>
#include <QStack>
#include <QLocale>
#include <QMatrix4x4>
SvgPathLoader::SvgPathLoader()
{
@ -20,6 +23,7 @@ struct SvgState
QString fillColor = {};
QString strokeColor = {};
QString strokeWidth = {};
QMatrix4x4 transform;
};
void SvgPathLoader::loadPaths()
@ -28,6 +32,7 @@ void SvgPathLoader::loadPaths()
m_fillColors.clear();
m_strokeColors.clear();
m_strokeWidths.clear();
m_transforms.clear();
if (m_source.isEmpty())
return;
@ -49,8 +54,50 @@ void SvgPathLoader::loadPaths()
QXmlStreamAttributes attrs = reader.attributes();
if (reader.isStartElement())
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"))) {
currentState.fillColor = attrs.value(QStringLiteral("fill")).toString();
if (!currentState.fillColor.startsWith("#"))
currentState.fillColor = "";
} else if (attrs.hasAttribute(QStringLiteral("style"))) {
QString s = attrs.value(QStringLiteral("style")).toString();
int idx = s.indexOf(QStringLiteral("fill:"));
@ -73,6 +120,22 @@ void SvgPathLoader::loadPaths()
m_fillColors.append(currentState.fillColor);
m_strokeColors.append(currentState.strokeColor);
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"))) {
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 strokeColors READ strokeColors NOTIFY pathsChanged)
Q_PROPERTY(QStringList strokeWidths READ strokeWidths NOTIFY pathsChanged)
Q_PROPERTY(QStringList transforms READ transforms NOTIFY pathsChanged)
public:
SvgPathLoader();
@ -53,6 +54,11 @@ public:
return m_strokeWidths;
}
QStringList transforms() const
{
return m_transforms;
}
private slots:
void loadPaths();
@ -66,6 +72,7 @@ private:
QStringList m_fillColors;
QStringList m_strokeColors;
QStringList m_strokeWidths;
QStringList m_transforms;
};
#endif // SVGPATHLOADER_H