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:
parent
2b9aa54610
commit
4a1e5d8e74
|
@ -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
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue