aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Olav Tvete <paul.tvete@qt.io>2023-08-16 15:44:59 +0200
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2023-08-18 17:49:51 +0000
commit84d719baab1f5415cb653337716e1d1e2754babc (patch)
tree49911f7395785f4527fc1410d387783fab60d96f
parent21de75df060c69357e004739421dc453a005af7a (diff)
Further Qt Quick Shapes curve renderer improvements
Add AA offset for strokes in the vertex shader * Add a normal vector to the vertex data, so we know the direction to move each point. Move the HG calculation for solving the cubic to the vertex shader (done by Matthias). * Also split up the curve elements for path stroking, to create tighter fitting triangles. (This does reveal some cases where the triangulation algorithm fails, which were previously masked by the 2x safety margin) * Also improve the triangulation by using the inner bisector when possible, leading to significantly less overdraw. As part of this, customTriangulator2() got a major refactor. It now has separate logic for inside and outside points. Remove debug color from vertex attributes * We make this a uniform instead, like in the stroke node, and calculate the debug color from the curvature data. With this path, we no longer show internal triangles as green, but otherwise it should be correct. Also improved documentation and various code cleanups and refactoring Done-with: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io> Done-with: Eirik Aavitsland <eirik.aavitsland@qt.io> Task-number: QTBUG-104122 Change-Id: Ib2817177eaaf4a0b1196cae77a28fca39349db9c Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io> Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io> (cherry picked from commit ec6c05f4ce5763ae85c1e9f7fb50c4f593709cc9) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--src/quickshapes/CMakeLists.txt2
-rw-r--r--src/quickshapes/qquadpath.cpp799
-rw-r--r--src/quickshapes/qquadpath_p.h257
-rw-r--r--src/quickshapes/qquickshape.cpp52
-rw-r--r--src/quickshapes/qquickshapecurvenode.cpp5
-rw-r--r--src/quickshapes/qquickshapecurvenode_p.cpp14
-rw-r--r--src/quickshapes/qquickshapecurvenode_p.h24
-rw-r--r--src/quickshapes/qquickshapecurverenderer.cpp1316
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p.h238
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p_p.h6
-rw-r--r--src/quickshapes/qquickshapestrokenode.cpp80
-rw-r--r--src/quickshapes/qquickshapestrokenode_p.cpp8
-rw-r--r--src/quickshapes/qquickshapestrokenode_p.h28
-rw-r--r--src/quickshapes/qt_quadratic_bezier.cpp193
-rw-r--r--src/quickshapes/shaders_ng/shapecurve.frag20
-rw-r--r--src/quickshapes/shaders_ng/shapecurve.vert14
-rw-r--r--src/quickshapes/shaders_ng/shapestroke.frag6
-rw-r--r--src/quickshapes/shaders_ng/shapestroke.vert44
-rw-r--r--tests/manual/painterpathquickshape/ControlPoint.qml1
-rw-r--r--tests/manual/painterpathquickshape/background.pngbin5898 -> 5397 bytes
-rw-r--r--tests/manual/painterpathquickshape/main.qml4
21 files changed, 1620 insertions, 1491 deletions
diff --git a/src/quickshapes/CMakeLists.txt b/src/quickshapes/CMakeLists.txt
index 8171b9f434..061a1c0981 100644
--- a/src/quickshapes/CMakeLists.txt
+++ b/src/quickshapes/CMakeLists.txt
@@ -23,7 +23,7 @@ qt_internal_add_qml_module(QuickShapesPrivate
qquickshapecurverenderer.cpp qquickshapecurverenderer_p.h qquickshapecurverenderer_p_p.h
qquickshapecurvenode.cpp qquickshapecurvenode_p.h qquickshapecurvenode_p_p.h qquickshapecurvenode_p.cpp
qquickshapestrokenode.cpp qquickshapestrokenode_p.h qquickshapestrokenode_p_p.h qquickshapestrokenode_p.cpp
- qt_quadratic_bezier.cpp
+ qquadpath_p.h qquadpath.cpp
qquickshapesoftwarerenderer.cpp qquickshapesoftwarerenderer_p.h
PUBLIC_LIBRARIES
Qt::Core
diff --git a/src/quickshapes/qquadpath.cpp b/src/quickshapes/qquadpath.cpp
new file mode 100644
index 0000000000..350c792cc5
--- /dev/null
+++ b/src/quickshapes/qquadpath.cpp
@@ -0,0 +1,799 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquadpath_p.h"
+#include <QtGui/private/qbezier_p.h>
+#include <QtMath>
+#include <QtCore/QVarLengthArray>
+
+QT_BEGIN_NAMESPACE
+
+static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp)
+{
+ 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 QPointF qt_quadraticForCubic(const QBezier &b)
+{
+ const QLineF st = b.startTangent();
+ const QLineF et = b.endTangent();
+ 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)
+{
+ auto isValidRoot = [](qreal r) {
+ return qIsFinite(r) && (r > 0) && (!qFuzzyIsNull(float(r))) && (r < 1)
+ && (!qFuzzyIsNull(float(r - 1)));
+ };
+
+ // normalize so pt1.x,pt1.y,pt4.y == 0
+ QTransform xf;
+ const QLineF l(orig.pt1(), orig.pt4());
+ xf.rotate(l.angle());
+ xf.translate(-orig.pt1().x(), -orig.pt1().y());
+ const QBezier n = orig.mapBy(xf);
+ Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().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 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) - (4 * a * c);
+ if (rad < 0)
+ return 0;
+ const qreal sqr = qSqrt(rad);
+ const qreal root1 = (-b + sqr) / (2 * a);
+ const qreal root2 = (-b - sqr) / (2 * a);
+
+ int res = 0;
+ if (isValidRoot(root1))
+ tpoints[res++] = root1;
+ if (root2 != root1 && isValidRoot(root2))
+ tpoints[res++] = root2;
+
+ if (res == 2 && tpoints[0] > tpoints[1])
+ qSwap(tpoints[0], tpoints[1]);
+
+ return res;
+}
+
+static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff)
+{
+ 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, maxSplits - 1, maxDiff);
+ qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff);
+ }
+}
+
+static void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit = 0.01)
+{
+ 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;
+ }
+ }
+
+ const QRectF cpr = b.bounds();
+ 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;
+ // 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, out, maxSubSplits, maxDiff);
+ t0 = t1;
+ }
+}
+
+QVector2D QQuadPath::Element::pointAtFraction(float t) const
+{
+ if (isLine()) {
+ return sp + t * (ep - sp);
+ } else {
+ const float r = 1 - t;
+ return (r * r * sp) + (2 * t * r * cp) + (t * t * ep);
+ }
+}
+
+float QQuadPath::Element::extent() const
+{
+ // TBD: cache this value if we start using it a lot
+ QVector2D min(qMin(sp.x(), ep.x()), qMin(sp.y(), ep.y()));
+ QVector2D max(qMax(sp.x(), ep.x()), qMax(sp.y(), ep.y()));
+ if (!isLine()) {
+ min = QVector2D(qMin(min.x(), cp.x()), qMin(min.y(), cp.y()));
+ max = QVector2D(qMax(max.x(), cp.x()), qMax(max.y(), cp.y()));
+ }
+ return (max - min).length();
+}
+
+// Returns the number of intersections between element and a horizontal line at y.
+// The t values of max 2 intersection(s) are stored in the fractions array
+int QQuadPath::Element::intersectionsAtY(float y, float *fractions) const
+{
+ const float y0 = startPoint().y() - y;
+ const float y1 = controlPoint().y() - y;
+ const float y2 = endPoint().y() - y;
+
+ int numRoots = 0;
+ const float a = y0 - (2 * y1) + y2;
+ if (a) {
+ const float b = (y1 * y1) - (y0 * y2);
+ if (b >= 0) {
+ const float sqr = qSqrt(b);
+ const float root1 = -(-y0 + y1 + sqr) / a;
+ if (qIsFinite(root1) && root1 >= 0 && root1 <= 1)
+ fractions[numRoots++] = root1;
+ const float root2 = (y0 - y1 + sqr) / a;
+ if (qIsFinite(root2) && root2 != root1 && root2 >= 0 && root2 <= 1)
+ fractions[numRoots++] = root2;
+ }
+ } else if (y1 != y2) {
+ const float root1 = (y2 - (2 * y1)) / (2 * (y2 - y1));
+ if (qIsFinite(root1) && root1 >= 0 && root1 <= 1)
+ fractions[numRoots++] = root1;
+ }
+
+ return numRoots;
+}
+
+static float crossProduct(const QVector2D &sp, const QVector2D &p, const QVector2D &ep)
+{
+ QVector2D v1 = ep - sp;
+ QVector2D v2 = p - sp;
+ return (v2.x() * v1.y()) - (v2.y() * v1.x());
+}
+
+bool QQuadPath::isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
+{
+ // Use cross product to compare directions of base vector and vector from start to p
+ return crossProduct(sp, p, ep) >= 0.0f;
+}
+
+bool QQuadPath::isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
+{
+ return qFuzzyIsNull(crossProduct(sp, p, ep));
+}
+
+// Assumes sp != ep
+bool QQuadPath::isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
+{
+ // epsilon is max length of p-to-baseline relative to length of baseline. So 0.01 means that
+ // the distance from p to the baseline must be less than 1% of the length of the baseline.
+ constexpr float epsilon = 0.01f;
+ QVector2D bv = ep - sp;
+ float bl2 = QVector2D::dotProduct(bv, bv);
+ float t = QVector2D::dotProduct(p - sp, bv) / bl2;
+ QVector2D pv = p - (sp + t * bv);
+ return (QVector2D::dotProduct(pv, pv) / bl2) < (epsilon * epsilon);
+}
+
+QVector2D QQuadPath::closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
+{
+ QVector2D line = ep - sp;
+ float t = QVector2D::dotProduct(p - sp, line) / QVector2D::dotProduct(line, line);
+ return sp + qBound(0.0f, t, 1.0f) * line;
+}
+
+// NOTE: it is assumed that subpaths are closed
+bool QQuadPath::contains(const QVector2D &point) const
+{
+ // if (!controlPointRect().contains(pt) : good opt when we add cpr caching
+ // return false;
+
+ int winding_number = 0;
+ for (const Element &e : m_elements) {
+ int dir = 1;
+ float y1 = e.startPoint().y();
+ float y2 = e.endPoint().y();
+ if (y2 < y1) {
+ qSwap(y1, y2);
+ dir = -1;
+ }
+ if (e.m_isLine) {
+ if (point.y() < y1 || point.y() >= y2 || y1 == y2)
+ continue;
+ const float t = (point.y() - e.startPoint().y()) / (e.endPoint().y() - e.startPoint().y());
+ const float x = e.startPoint().x() + t * (e.endPoint().x() - e.startPoint().x());
+ if (x <= point.x())
+ winding_number += dir;
+ } else {
+ y1 = qMin(y1, e.controlPoint().y());
+ y2 = qMax(y2, e.controlPoint().y());
+ if (point.y() < y1 || point.y() >= y2)
+ continue;
+ float ts[2];
+ const int numRoots = e.intersectionsAtY(point.y(), ts);
+ // Count if there is exactly one intersection to the left
+ bool oneHit = false;
+ float tForHit = -1;
+ for (int i = 0; i < numRoots; i++) {
+ if (e.pointAtFraction(ts[i]).x() <= point.x()) {
+ oneHit = !oneHit;
+ tForHit = ts[i];
+ }
+ }
+ if (oneHit) {
+ dir = e.tangentAtFraction(tForHit).y() < 0 ? -1 : 1;
+ winding_number += dir;
+ }
+ }
+ };
+
+ return (fillRule() == Qt::WindingFill ? (winding_number != 0) : ((winding_number % 2) != 0));
+}
+
+void QQuadPath::addElement(const QVector2D &control, const QVector2D &endPoint, bool isLine)
+{
+ if (qFuzzyCompare(currentPoint, endPoint))
+ return; // 0 length element, skip
+
+ isLine = isLine || isPointNearLine(control, currentPoint, endPoint); // Turn flat quad into line
+
+ m_elements.resize(m_elements.size() + 1);
+ Element &elem = m_elements.last();
+ elem.sp = currentPoint;
+ elem.cp = isLine ? (0.5f * (currentPoint + endPoint)) : control;
+ elem.ep = endPoint;
+ elem.m_isLine = isLine;
+ elem.m_isSubpathStart = subPathToStart;
+ subPathToStart = false;
+ currentPoint = endPoint;
+}
+
+#if !defined(QQUADPATH_CONVEX_CHECK_ERROR_MARGIN)
+# define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN (1.0f / 32.0f)
+#endif
+
+QQuadPath::Element::CurvatureFlags QQuadPath::coordinateOrderOfElement(const QQuadPath::Element &element) const
+{
+ QVector2D baseLine = element.endPoint() - element.startPoint();
+ QVector2D midPoint = element.midPoint();
+ // At the midpoint, the tangent of a quad is parallel to the baseline
+ QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
+ float delta = qMin(element.extent() / 100, QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN);
+ QVector2D justRightOfMid = midPoint + (normal * delta);
+ bool pathContainsPoint = contains(justRightOfMid);
+ return pathContainsPoint ? Element::FillOnRight : Element::CurvatureFlags(0);
+}
+
+QQuadPath QQuadPath::fromPainterPath(const QPainterPath &path)
+{
+ QQuadPath res;
+ res.reserve(path.elementCount());
+ res.setFillRule(path.fillRule());
+
+ QPolygonF quads;
+ QPointF sp;
+ for (int i = 0; i < path.elementCount(); ++i) {
+ QPainterPath::Element element = path.elementAt(i);
+
+ QPointF ep(element);
+ switch (element.type) {
+ case QPainterPath::MoveToElement:
+ res.moveTo(QVector2D(ep));
+ break;
+ case QPainterPath::LineToElement:
+ res.lineTo(QVector2D(ep));
+ break;
+ case QPainterPath::CurveToElement: {
+ QPointF cp1 = ep;
+ QPointF cp2(path.elementAt(++i));
+ ep = path.elementAt(++i);
+ QBezier b = QBezier::fromPoints(sp, cp1, cp2, ep);
+ qt_toQuadratics(b, &quads);
+ for (int i = 1; i < quads.size(); i += 2) {
+ QVector2D cp(quads[i]);
+ QVector2D ep(quads[i + 1]);
+ res.quadTo(cp, ep);
+ }
+ break;
+ }
+ default:
+ Q_UNREACHABLE();
+ break;
+ }
+ sp = ep;
+ }
+
+ return res;
+}
+
+void QQuadPath::addCurvatureData()
+{
+ // We use the convention that the inside of a curve is on the *right* side of the
+ // direction of the baseline.Thus, as long as this is true: if the control point is
+ // on the left side of the baseline, the curve is convex and otherwise it is
+ // concave. The paths we get can be arbitrary order, but each subpath will have a
+ // consistent order. Therefore, for the first curve element in a subpath, we can
+ // determine if the direction already follows the convention or not, and then we
+ // can easily detect curvature of all subsequent elements in the subpath.
+
+ static bool checkAnomaly = qEnvironmentVariableIntValue("QT_QUICKSHAPES_CHECK_ALL_CURVATURE") != 0;
+
+ Element::CurvatureFlags flags = Element::CurvatureUndetermined;
+ for (QQuadPath::Element &element : m_elements) {
+ Q_ASSERT(element.childCount() == 0);
+ if (element.isSubpathStart()) {
+ flags = coordinateOrderOfElement(element);
+ } else if (checkAnomaly) {
+ Element::CurvatureFlags newFlags = coordinateOrderOfElement(element);
+ if (flags != newFlags) {
+ qDebug() << "Curvature anomaly detected:" << element
+ << "Subpath fill on right:" << (flags & Element::FillOnRight)
+ << "Element fill on right:" << (newFlags & Element::FillOnRight);
+ flags = newFlags;
+ }
+ }
+
+ if (element.isLine()) {
+ element.m_curvatureFlags = flags;
+ // Set the control point to an arbitrary point on the inside side of the line
+ // (doesn't need to actually be inside the shape: it just makes our calculations
+ // easier later if it is at the same side as the fill).
+ const QVector2D &sp = element.sp;
+ const QVector2D &ep = element.ep;
+ QVector2D v = ep - sp;
+ element.cp = flags & Element::FillOnRight ? sp + QVector2D(-v.y(), v.x()) : sp + QVector2D(v.y(), -v.x());
+ } else {
+ bool controlPointOnLeft = element.isControlPointOnLeft();
+ bool isFillOnRight = flags & Element::FillOnRight;
+ bool isConvex = controlPointOnLeft == isFillOnRight;
+
+ if (isConvex)
+ element.m_curvatureFlags = Element::CurvatureFlags(flags | Element::Convex);
+ else
+ element.m_curvatureFlags = flags;
+ }
+ }
+}
+
+QRectF QQuadPath::controlPointRect() const
+{
+ QRectF res;
+ if (elementCount()) {
+ QVector2D min, max;
+ min = max = m_elements.constFirst().sp;
+ // No need to recurse, as split curve's controlpoints are within the parent curve's
+ for (const QQuadPath::Element &e : std::as_const(m_elements)) {
+ min.setX(std::min({ min.x(), e.sp.x(), e.cp.x(), e.ep.x() }));
+ min.setY(std::min({ min.y(), e.sp.y(), e.cp.y(), e.ep.y() }));
+ max.setX(std::max({ max.x(), e.sp.x(), e.cp.x(), e.ep.x() }));
+ max.setY(std::max({ max.y(), e.sp.y(), e.cp.y(), e.ep.y() }));
+ }
+ res = QRectF(min.toPointF(), max.toPointF());
+ }
+ return res;
+}
+
+// Count leaf elements
+int QQuadPath::elementCountRecursive() const
+{
+ int count = 0;
+ iterateElements([&](const QQuadPath::Element &) { count++; });
+ return count;
+}
+
+QPainterPath QQuadPath::toPainterPath() const
+{
+ // Currently only converts the main, unsplit path; no need for the split path identified
+ QPainterPath res;
+ res.reserve(elementCount());
+ res.setFillRule(fillRule());
+ for (const Element &element : m_elements) {
+ if (element.m_isSubpathStart)
+ res.moveTo(element.startPoint().toPointF());
+ if (element.m_isLine)
+ res.lineTo(element.endPoint().toPointF());
+ else
+ res.quadTo(element.controlPoint().toPointF(), element.endPoint().toPointF());
+ };
+ return res;
+}
+
+// Returns a new path since doing it inline would probably be less efficient
+// (technically changing it from O(n) to O(n^2))
+// Note that this function should be called before splitting any elements,
+// so we can assume that the structure is a list and not a tree
+QQuadPath QQuadPath::subPathsClosed() const
+{
+ Q_ASSERT(m_childElements.isEmpty());
+
+ QQuadPath res = *this;
+ res.m_elements = {};
+ res.m_elements.reserve(elementCount());
+ int subStart = -1;
+ int prevElement = -1;
+ for (int i = 0; i < elementCount(); i++) {
+ const auto &element = m_elements.at(i);
+ if (element.m_isSubpathStart) {
+ if (subStart >= 0 && m_elements[i - 1].ep != m_elements[subStart].sp) {
+ res.currentPoint = m_elements[i - 1].ep;
+ res.lineTo(m_elements[subStart].sp);
+ auto &endElement = res.m_elements.last();
+ endElement.m_isSubpathEnd = true;
+ // lineTo() can bail out if the points are too close.
+ // In that case, just change the end point to be equal to the start
+ // (No need to test because the assignment is a no-op otherwise).
+ endElement.ep = m_elements[subStart].sp;
+ } else if (prevElement >= 0) {
+ res.m_elements[prevElement].m_isSubpathEnd = true;
+ }
+ subStart = i;
+ }
+ res.m_elements.append(element);
+ prevElement = res.m_elements.size() - 1;
+ }
+
+ if (subStart >= 0 && m_elements.last().ep != m_elements[subStart].sp) {
+ res.currentPoint = m_elements.last().ep;
+ res.lineTo(m_elements[subStart].sp);
+ }
+ if (!res.m_elements.isEmpty()) {
+ auto &endElement = res.m_elements.last();
+ endElement.m_isSubpathEnd = true;
+ endElement.ep = m_elements[subStart].sp;
+ }
+
+ // ### Workaround for triangulator issue: Avoid 3-element paths
+ if (res.elementCount() == 3) {
+ res.splitElementAt(2);
+ res = res.flattened();
+ Q_ASSERT(res.elementCount() == 4);
+ }
+
+ return res;
+}
+
+QQuadPath QQuadPath::flattened() const
+{
+ QQuadPath res;
+ res.reserve(elementCountRecursive());
+ iterateElements([&](const QQuadPath::Element &element) { res.m_elements.append(element); });
+ return res;
+}
+
+class ElementCutter
+{
+public:
+ ElementCutter(const QQuadPath::Element &element)
+ : m_element(element)
+ {
+ m_currentPoint = m_element.startPoint();
+ if (m_element.isLine())
+ m_lineLength = (m_element.endPoint() - m_element.startPoint()).length();
+ else
+ fillLUT();
+ }
+
+ bool consume(float length)
+ {
+ m_lastT = m_currentT;
+ m_lastPoint = m_currentPoint;
+ float nextCut = m_consumed + length;
+ float cutT = m_element.isLine() ? nextCut / m_lineLength : tForLength(nextCut);
+ if (cutT < 1) {
+ m_currentT = cutT;
+ m_currentPoint = m_element.pointAtFraction(m_currentT);
+ m_consumed = nextCut;
+ return true;
+ } else {
+ m_currentT = 1;
+ m_currentPoint = m_element.endPoint();
+ return false;
+ }
+ }
+
+ QVector2D currentCutPoint()
+ {
+ return m_currentPoint;
+ }
+
+ QVector2D currentControlPoint()
+ {
+ Q_ASSERT(!m_element.isLine());
+ // Split curve right at lastT, yields { lastPoint, rcp, endPoint } quad segment
+ QVector2D rcp = (1 - m_lastT) * m_element.controlPoint() + m_lastT * m_element.endPoint();
+ // Split that left at currentT, yields { lastPoint, lcp, currentPoint } quad segment
+ float segmentT = (m_currentT - m_lastT) / (1 - m_lastT);
+ QVector2D lcp = (1 - segmentT) * m_lastPoint + segmentT * rcp;
+ return lcp;
+ }
+
+ float lastLength()
+ {
+ float elemLength = m_element.isLine() ? m_lineLength : m_lut.last();
+ return elemLength - m_consumed;
+ }
+
+private:
+ void fillLUT()
+ {
+ Q_ASSERT(!m_element.isLine());
+ QVector2D ap = m_element.startPoint() - 2 * m_element.controlPoint() + m_element.endPoint();
+ QVector2D bp = 2 * m_element.controlPoint() - 2 * m_element.startPoint();
+ float A = 4 * QVector2D::dotProduct(ap, ap);
+ float B = 4 * QVector2D::dotProduct(ap, bp);
+ float C = QVector2D::dotProduct(bp, bp);
+ float b = B / (2 * A);
+ float c = C / A;
+ float k = c - (b * b);
+ float l2 = b * std::sqrt(b * b + k);
+ float lnom = b + std::sqrt(b * b + k);
+ float l0 = 0.5f * std::sqrt(A);
+
+ m_lut.resize(LUTSize, 0);
+ for (int i = 1; i < LUTSize; i++) {
+ float t = float(i) / (LUTSize - 1);
+ float u = t + b;
+ float w = std::sqrt(u * u + k);
+ float l1 = u * w;
+ float lden = u + w;
+ float l3 = k * std::log(std::fabs(lden / lnom));
+ float res = l0 * (l1 - l2 + l3);
+ m_lut[i] = res;
+ }
+ }
+
+ float tForLength(float length)
+ {
+ Q_ASSERT(!m_element.isLine());
+ Q_ASSERT(!m_lut.isEmpty());
+
+ float res = 2; // I.e. invalid, outside [0, 1] range
+ auto it = std::upper_bound(m_lut.cbegin(), m_lut.cend(), length);
+ if (it != m_lut.cend()) {
+ float nextLength = *it--;
+ float prevLength = *it;
+ int prevIndex = std::distance(m_lut.cbegin(), it);
+ float fraction = (length - prevLength) / (nextLength - prevLength);
+ res = (prevIndex + fraction) / (LUTSize - 1);
+ }
+ return res;
+ }
+
+ const QQuadPath::Element &m_element;
+ float m_lastT = 0;
+ float m_currentT = 0;
+ QVector2D m_lastPoint;
+ QVector2D m_currentPoint;
+ float m_consumed = 0;
+ // For line elements:
+ float m_lineLength;
+ // For quadratic curve elements:
+ static constexpr int LUTSize = 21;
+ QVarLengthArray<float, LUTSize> m_lut;
+};
+
+QQuadPath QQuadPath::dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset) const
+{
+ QVarLengthArray<float, 16> pattern;
+ float patternLength = 0;
+ for (int i = 0; i < 2 * (dashPattern.length() / 2); i++) {
+ float dashLength = qMax(lineWidth * dashPattern[i], qreal(0));
+ pattern.append(dashLength);
+ patternLength += dashLength;
+ }
+ if (patternLength == 0)
+ return {};
+
+ int startIndex = 0;
+ float startOffset = std::fmod(lineWidth * dashOffset, patternLength);
+ if (startOffset < 0)
+ startOffset += patternLength;
+ for (float dashLength : pattern) {
+ if (dashLength > startOffset)
+ break;
+ startIndex++;
+ startOffset -= dashLength;
+ }
+
+ int dashIndex = startIndex;
+ float offset = startOffset;
+ QQuadPath res;
+ for (int i = 0; i < elementCount(); i++) {
+ const Element &element = elementAt(i);
+ if (element.isSubpathStart()) {
+ res.moveTo(element.startPoint());
+ dashIndex = startIndex;
+ offset = startOffset;
+ }
+ ElementCutter cutter(element);
+ while (true) {
+ bool gotAll = cutter.consume(pattern.at(dashIndex) - offset);
+ QVector2D nextPoint = cutter.currentCutPoint();
+ if (dashIndex & 1)
+ res.moveTo(nextPoint); // gap
+ else if (element.isLine())
+ res.lineTo(nextPoint); // dash in line
+ else
+ res.quadTo(cutter.currentControlPoint(), nextPoint); // dash in curve
+ if (gotAll) {
+ offset = 0;
+ dashIndex = (dashIndex + 1) % pattern.size();
+ } else {
+ offset += cutter.lastLength();
+ break;
+ }
+ }
+ }
+ return res;
+}
+
+void QQuadPath::splitElementAt(int index)
+{
+ const int newChildIndex = m_childElements.size();
+ m_childElements.resize(newChildIndex + 2);
+ Element &parent = elementAt(index);
+ parent.m_numChildren = 2;
+ parent.m_firstChildIndex = newChildIndex;
+
+ Element &quad1 = m_childElements[newChildIndex];
+ const QVector2D mp = parent.midPoint();
+ quad1.sp = parent.sp;
+ quad1.cp = 0.5f * (parent.sp + parent.cp);
+ quad1.ep = mp;
+ quad1.m_isSubpathStart = parent.m_isSubpathStart;
+ quad1.m_isSubpathEnd = false;
+ quad1.m_curvatureFlags = parent.m_curvatureFlags;
+ quad1.m_isLine = parent.m_isLine; //### || isPointNearLine(quad1.cp, quad1.sp, quad1.ep);
+
+ Element &quad2 = m_childElements[newChildIndex + 1];
+ quad2.sp = mp;
+ quad2.cp = 0.5f * (parent.ep + parent.cp);
+ quad2.ep = parent.ep;
+ quad2.m_isSubpathStart = false;
+ quad2.m_isSubpathEnd = parent.m_isSubpathEnd;
+ quad2.m_curvatureFlags = parent.m_curvatureFlags;
+ quad2.m_isLine = parent.m_isLine; //### || isPointNearLine(quad2.cp, quad2.sp, quad2.ep);
+
+#ifndef QT_NO_DEBUG
+ if (qFuzzyCompare(quad1.sp, quad1.ep) || qFuzzyCompare(quad2.sp, quad2.ep))
+ qDebug() << "###FIXME: quad splitting has yielded ~null quad.";
+#endif
+}
+
+static void printElement(QDebug stream, const QQuadPath::Element &element)
+{
+ auto printPoint = [&](QVector2D p) { stream << "(" << p.x() << ", " << p.y() << ") "; };
+ stream << "{ ";
+ printPoint(element.startPoint());
+ printPoint(element.controlPoint());
+ printPoint(element.endPoint());
+ stream << "} " << (element.isLine() ? "L " : "C ") << (element.isConvex() ? "X " : "O ")
+ << (element.isSubpathStart() ? "S" : element.isSubpathEnd() ? "E" : "");
+}
+
+QDebug operator<<(QDebug stream, const QQuadPath::Element &element)
+{
+ QDebugStateSaver saver(stream);
+ stream.nospace();
+ stream << "QuadPath::Element( ";
+ printElement(stream, element);
+ stream << " )";
+ return stream;
+}
+
+QDebug operator<<(QDebug stream, const QQuadPath &path)
+{
+ QDebugStateSaver saver(stream);
+ stream.nospace();
+ stream << "QuadPath(" << path.elementCount() << " main elements, "
+ << path.elementCountRecursive() << " leaf elements, "
+ << (path.fillRule() == Qt::OddEvenFill ? "OddEven" : "Winding") << Qt::endl;
+ int count = 0;
+ path.iterateElements([&](const QQuadPath::Element &e) {
+ stream << " " << count++ << (e.isSubpathStart() ? " >" : " ");
+ printElement(stream, e);
+ stream << Qt::endl;
+ });
+ stream << ")";
+ return stream;
+}
+
+QT_END_NAMESPACE
diff --git a/src/quickshapes/qquadpath_p.h b/src/quickshapes/qquadpath_p.h
new file mode 100644
index 0000000000..e6b038c09c
--- /dev/null
+++ b/src/quickshapes/qquadpath_p.h
@@ -0,0 +1,257 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUADPATH_P_H
+#define QQUADPATH_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of a number of Qt sources files. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qrect.h>
+#include <QtCore/qlist.h>
+#include <QtCore/qdebug.h>
+#include <QtGui/qvector2d.h>
+#include <QtGui/qpainterpath.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQuadPath
+{
+public:
+ class Element
+ {
+ public:
+ Element ()
+ : m_isSubpathStart(false), m_isSubpathEnd(false), m_isLine(false)
+ {
+ }
+
+ bool isSubpathStart() const
+ {
+ return m_isSubpathStart;
+ }
+
+ bool isSubpathEnd() const
+ {
+ return m_isSubpathEnd;
+ }
+
+ bool isLine() const
+ {
+ return m_isLine;
+ }
+
+ bool isConvex() const
+ {
+ return m_curvatureFlags & Convex;
+ }
+
+ QVector2D startPoint() const
+ {
+ return sp;
+ }
+
+ QVector2D controlPoint() const
+ {
+ return cp;
+ }
+
+ QVector2D endPoint() const
+ {
+ return ep;
+ }
+
+ QVector2D midPoint() const
+ {
+ return isLine() ? 0.5f * (sp + ep) : (0.25f * sp) + (0.5f * cp) + (0.25 * ep);
+ }
+
+ int childCount() const { return m_numChildren; }
+
+ int indexOfChild(int childNumber) const
+ {
+ Q_ASSERT(childNumber >= 0 && childNumber < childCount());
+ return -(m_firstChildIndex + 1 + childNumber);
+ }
+
+ QVector2D pointAtFraction(float t) const;
+
+ QVector2D tangentAtFraction(float t) const
+ {
+ return isLine() ? (ep - sp) : ((1 - t) * 2 * (cp - sp)) + (t * 2 * (ep - cp));
+ }
+
+ QVector2D normalAtFraction(float t) const
+ {
+ const QVector2D tan = tangentAtFraction(t);
+ return QVector2D(-tan.y(), tan.x());
+ }
+
+ float extent() const;
+
+ void setAsConvex(bool isConvex)
+ {
+ if (isConvex)
+ m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags | Element::Convex);
+ else
+ m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::Convex);
+ }
+
+ bool isControlPointOnLeft() const
+ {
+ return isPointOnLeft(cp, sp, ep);
+ }
+
+ private:
+ int intersectionsAtY(float y, float *fractions) const;
+
+ enum CurvatureFlags : quint8 {
+ CurvatureUndetermined = 0,
+ FillOnRight = 1,
+ Convex = 2
+ };
+
+ QVector2D sp;
+ QVector2D cp;
+ QVector2D ep;
+ int m_firstChildIndex = 0;
+ quint8 m_numChildren = 0;
+ CurvatureFlags m_curvatureFlags = CurvatureUndetermined;
+ quint8 m_isSubpathStart : 1;
+ quint8 m_isSubpathEnd : 1;
+ quint8 m_isLine : 1;
+ friend class QQuadPath;
+ friend QDebug operator<<(QDebug, const QQuadPath::Element &);
+ };
+
+ void moveTo(const QVector2D &to)
+ {
+ subPathToStart = true;
+ currentPoint = to;
+ }
+
+ void lineTo(const QVector2D &to)
+ {
+ addElement({}, to, true);
+ }
+
+ void quadTo(const QVector2D &control, const QVector2D &to)
+ {
+ addElement(control, to);
+ }
+
+ Element &elementAt(int i)
+ {
+ return i < 0 ? m_childElements[-(i + 1)] : m_elements[i];
+ }
+
+ const Element &elementAt(int i) const
+ {
+ return i < 0 ? m_childElements[-(i + 1)] : m_elements[i];
+ }
+
+ int indexOfChildAt(int i, int childNumber) const
+ {
+ return elementAt(i).indexOfChild(childNumber);
+ }
+
+ QRectF controlPointRect() const;
+
+ Qt::FillRule fillRule() const { return m_fillRule; }
+ void setFillRule(Qt::FillRule rule) { m_fillRule = rule; }
+
+ void reserve(int size) { m_elements.reserve(size); }
+ int elementCount() const { return m_elements.size(); }
+ bool isEmpty() const { return m_elements.size() == 0; }
+ int elementCountRecursive() const;
+
+ static QQuadPath fromPainterPath(const QPainterPath &path);
+ QPainterPath toPainterPath() const;
+
+ QQuadPath subPathsClosed() const;
+ void addCurvatureData();
+ QQuadPath flattened() const;
+ QQuadPath dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset = 0) const;
+ void splitElementAt(int index);
+ bool contains(const QVector2D &point) const;
+
+ template<typename Func>
+ void iterateChildrenOf(Element &e, Func &&lambda)
+ {
+ const int lastChildIndex = e.m_firstChildIndex + e.childCount() - 1;
+ for (int i = e.m_firstChildIndex; i <= lastChildIndex; i++) {
+ Element &c = m_childElements[i];
+ if (c.childCount() > 0)
+ iterateChildrenOf(c, lambda);
+ else
+ lambda(c);
+ }
+ }
+
+ template<typename Func>
+ void iterateChildrenOf(const Element &e, Func &&lambda) const
+ {
+ const int lastChildIndex = e.m_firstChildIndex + e.childCount() - 1;
+ for (int i = e.m_firstChildIndex; i <= lastChildIndex; i++) {
+ const Element &c = m_childElements[i];
+ if (c.childCount() > 0)
+ iterateChildrenOf(c, lambda);
+ else
+ lambda(c);
+ }
+ }
+
+ template<typename Func>
+ void iterateElements(Func &&lambda)
+ {
+ for (auto &e : m_elements) {
+ if (e.childCount() > 0)
+ iterateChildrenOf(e, lambda);
+ else
+ lambda(e);
+ }
+ }
+
+ template<typename Func>
+ void iterateElements(Func &&lambda) const
+ {
+ for (auto &e : m_elements) {
+ if (e.childCount() > 0)
+ iterateChildrenOf(e, lambda);
+ else
+ lambda(e);
+ }
+ }
+
+ static QVector2D closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
+ static bool isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
+ static bool isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
+ static bool isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
+
+private:
+ void addElement(const QVector2D &control, const QVector2D &to, bool isLine = false);
+ Element::CurvatureFlags coordinateOrderOfElement(const Element &element) const;
+
+ friend QDebug operator<<(QDebug, const QQuadPath &);
+
+ bool subPathToStart = true;
+ Qt::FillRule m_fillRule = Qt::OddEvenFill;
+ QVector2D currentPoint;
+ QList<Element> m_elements;
+ QList<Element> m_childElements;
+};
+
+QDebug operator<<(QDebug, const QQuadPath::Element &);
+QDebug operator<<(QDebug, const QQuadPath &);
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/quickshapes/qquickshape.cpp b/src/quickshapes/qquickshape.cpp
index 4f6282675b..9086742269 100644
--- a/src/quickshapes/qquickshape.cpp
+++ b/src/quickshapes/qquickshape.cpp
@@ -671,9 +671,9 @@ QQuickShape::~QQuickShape()
The renderer is unknown.
\value Shape.GeometryRenderer
- The generic, driver independent solution for OpenGL. Uses the same
+ The generic, driver independent solution for GPU rendering. Uses the same
CPU-based triangulation approach as QPainter's OpenGL 2 paint
- engine. This is the default when the OpenGL Qt Quick scenegraph
+ engine. This is the default when the RHI-based Qt Quick scenegraph
backend is in use.
\value Shape.SoftwareRenderer
@@ -682,44 +682,34 @@ QQuickShape::~QQuickShape()
with the \c software backend.
\value Shape.CurveRenderer
- Added as technology preview in Qt 6.6.
- Experimental renderer which triangulates the polygonal internal hull of the shape,
- similar to \c Shape.GeometryRenderer. But instead of also triangulating curved areas,
- this renderer renders curved areas using a specialized fragment shader. This means that
- the shape does not need to be re-tesselated when it changes size or is zoomed. For
- supported shapes, this can give improved runtime performance in cases where the shapes
- are repeatedly transformed.
+ Experimental GPU-based renderer, added as technology preview in Qt 6.6.
+ In contrast to \c Shape.GeometryRenderer, curves are not approximated by short straight
+ lines. Instead, curves are rendered using a specialized fragment shader. This improves
+ visual quality and avoids re-tesselation performance hit when zooming. Also,
+ \c Shape.CurveRenderer provides native, high-quality anti-aliasing, without the
+ performance cost of multi- or supersampling.
By default, \c Shape.GeometryRenderer will be selected unless the Qt Quick scenegraph is running
with the \c software backend. In that case, \c Shape.SoftwareRenderer will be used.
+ \c Shape.CurveRenderer may be requested using the \l preferredRendererType property.
- \c Shape.CurveRenderer can be optionally selected using the \l preferredRendererType property.
-
- In addition to rendering smooth curves regardless of zoom level, this renderer applies
- anti-aliasing without enabling MSAA on the surface, which may provide performance gain.
-
- Note that \c Shape.CurveRenderer is currently regarded as experimental and has several
- limitations:
+ Note that \c Shape.CurveRenderer is currently regarded as experimental. The enum name of
+ this renderer may change in future versions of Qt, and some shapes may render incorrectly.
+ Among the known limitations are:
\list 1
- \li The \c GL_OES_standard_derivatives extension to OpenGL is required when the OpenGL
- RHI backend is in use (this is available by default on OpenGL ES 3 and later, but
- optional in OpenGL ES 2).
- \li Only quadratic curves are supported (cubic curves will be approximated by quadratic
- curves).
- \li Shapes where elements intersect are not supported. Use the \l [QML] {Path::simplified}
- {Path.simplified} property to remove self-intersections from such shapes.
+ \li Only quadratic curves are inherently supported. Cubic curves will be approximated by
+ quadratic curves.
+ \li Shapes where elements intersect are not rendered correctly. The \l [QML] {Path::simplified}
+ {Path.simplified} property may be used to remove self-intersections from such shapes, but
+ may incur a performance cost and reduced visual quality.
\li Shapes that span a large numerical range, such as a long string of text, may have
issues. Consider splitting these shapes into multiple ones, for instance by making
a \l PathText for each individual word.
+ \li If the shape is being rendered into a Qt Quick 3D scene, the
+ \c GL_OES_standard_derivatives extension to OpenGL is required when the OpenGL
+ RHI backend is in use (this is available by default on OpenGL ES 3 and later, but
+ optional in OpenGL ES 2).
\endlist
-
- Due to the fact that the \c Shape.CurveRenderer approximates cubic curves, there are certain
- shapes it will not render accurately. For instance, circular arcs are not representable using quadratic
- curves and will only look approximately correct. If the visual representation is
- insufficient for a particular shape, consider using \c Shape.GeometryRenderer instead.
-
- \note The \c Shape.CurveRenderer is currently considered a tech preview, thus the name of
- this enum may change in future versions of Qt and some shapes may render incorrectly.
*/
QQuickShape::RendererType QQuickShape::rendererType() const
diff --git a/src/quickshapes/qquickshapecurvenode.cpp b/src/quickshapes/qquickshapecurvenode.cpp
index 06fd3b815e..6d104bdc17 100644
--- a/src/quickshapes/qquickshapecurvenode.cpp
+++ b/src/quickshapes/qquickshapecurvenode.cpp
@@ -53,10 +53,9 @@ const QSGGeometry::AttributeSet &QQuickShapeCurveNode::attributes()
static QSGGeometry::Attribute data[] = {
QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute),
QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
- QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::ColorAttribute),
- QSGGeometry::Attribute::createWithAttributeType(3, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute),
+ QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute),
};
- static QSGGeometry::AttributeSet attrs = { 4, sizeof(CurveNodeVertex), data };
+ static QSGGeometry::AttributeSet attrs = { 3, sizeof(CurveNodeVertex), data };
return attrs;
}
diff --git a/src/quickshapes/qquickshapecurvenode_p.cpp b/src/quickshapes/qquickshapecurvenode_p.cpp
index 19f2a22887..c19672d1eb 100644
--- a/src/quickshapes/qquickshapecurvenode_p.cpp
+++ b/src/quickshapes/qquickshapecurvenode_p.cpp
@@ -74,21 +74,20 @@ namespace {
const QMatrix4x4 m = state.combinedMatrix();
memcpy(buf->data() + offset, m.constData(), 64);
- offset += 64;
matrixScale = qSqrt(qAbs(state.determinant()));
- memcpy(buf->data() + offset, &matrixScale, 4);
- offset += 4;
+ memcpy(buf->data() + offset + 64, &matrixScale, 4);
changed = true;
}
+ offset += 68;
if (state.isOpacityDirty()) {
const float opacity = state.opacity();
memcpy(buf->data() + offset, &opacity, 4);
changed = true;
}
- offset += 12;
+ offset += 4;
QQuickShapeCurveMaterial *newMaterial = static_cast<QQuickShapeCurveMaterial *>(newEffect);
QQuickShapeCurveMaterial *oldMaterial = static_cast<QQuickShapeCurveMaterial *>(oldEffect);
@@ -99,6 +98,13 @@ namespace {
if (newNode == nullptr)
return changed;
+ if (oldNode == nullptr || oldNode->debug() != newNode->debug()) {
+ float debug = newNode->debug();
+ memcpy(buf->data() + offset, &debug, 4);
+ changed = true;
+ }
+ offset += 8;
+
if (newNode->hasStroke()) {
Q_ASSERT(buf->size() >= offset + 32);
QVector4D newStrokeColor(newNode->strokeColor().redF(),
diff --git a/src/quickshapes/qquickshapecurvenode_p.h b/src/quickshapes/qquickshapecurvenode_p.h
index 0e66ac1271..be27fa665d 100644
--- a/src/quickshapes/qquickshapecurvenode_p.h
+++ b/src/quickshapes/qquickshapecurvenode_p.h
@@ -80,6 +80,16 @@ public:
}
}
+ float debug() const
+ {
+ return m_debug;
+ }
+
+ void setDebug(float newDebug)
+ {
+ m_debug = newDebug;
+ }
+
QQuickAbstractPathRenderer::FillGradientType gradientType() const
{
return m_gradientType;
@@ -93,10 +103,7 @@ public:
void appendTriangle(const QVector2D &v1,
const QVector2D &v2,
const QVector2D &v3,
- std::function<QVector3D(QVector2D)> uvForPoint,
- QVector4D debugColor1,
- QVector4D debugColor2,
- QVector4D debugColor3)
+ std::function<QVector3D(QVector2D)> uvForPoint)
{
QVector3D uv1 = uvForPoint(v1);
QVector3D uv2 = uvForPoint(v2);
@@ -108,7 +115,6 @@ public:
m_uncookedIndexes.append(m_uncookedVertexes.size());
m_uncookedVertexes.append( { v1.x(), v1.y(),
uv1.x(), uv1.y(), uv1.z(),
- debugColor1.x(), debugColor1.y(), debugColor1.z(), debugColor1.w(),
duvdx.x(), duvdx.y(),
duvdy.x(), duvdy.y()
});
@@ -116,7 +122,6 @@ public:
m_uncookedIndexes.append(m_uncookedVertexes.size());
m_uncookedVertexes.append( { v2.x(), v2.y(),
uv2.x(), uv2.y(), uv2.z(),
- debugColor2.x(), debugColor2.y(), debugColor2.z(), debugColor2.w(),
duvdx.x(), duvdx.y(),
duvdy.x(), duvdy.y()
});
@@ -124,15 +129,13 @@ public:
m_uncookedIndexes.append(m_uncookedVertexes.size());
m_uncookedVertexes.append( { v3.x(), v3.y(),
uv3.x(), uv3.y(), uv3.z(),
- debugColor3.x(), debugColor3.y(), debugColor3.z(), debugColor3.w(),
duvdx.x(), duvdx.y(),
duvdy.x(), duvdy.y()
});
}
void appendVertex(const QVector2D &vertex,
- std::function<QVector3D(QVector2D)> uvForPoint,
- const QVector4D &debugColor)
+ std::function<QVector3D(QVector2D)> uvForPoint)
{
QVector3D uv = uvForPoint(vertex);
@@ -141,7 +144,6 @@ public:
m_uncookedVertexes.append( { vertex.x(), vertex.y(),
uv.x(), uv.y(), uv.z(),
- debugColor.x(), debugColor.y(), debugColor.z(), debugColor.w(),
duvdx.x(), duvdx.y(),
duvdy.x(), duvdy.y()
}
@@ -169,7 +171,6 @@ private:
struct CurveNodeVertex
{
float x, y, u, v, w;
- float r, g, b, a; // Debug color, mixed in proportion to a
float dudx, dvdx, dudy, dvdy; // Size of pixel in curve space (must be same for all vertices in triangle)
};
@@ -179,6 +180,7 @@ private:
QColor m_color = Qt::white;
QColor m_strokeColor = Qt::transparent;
float m_strokeWidth = 0.0f;
+ float m_debug = 0.0f;
QQuickAbstractPathRenderer::GradientDesc m_fillGradient;
QQuickAbstractPathRenderer::FillGradientType m_gradientType = QQuickAbstractPathRenderer::NoGradient;
diff --git a/src/quickshapes/qquickshapecurverenderer.cpp b/src/quickshapes/qquickshapecurverenderer.cpp
index a20db675e2..ab74288739 100644
--- a/src/quickshapes/qquickshapecurverenderer.cpp
+++ b/src/quickshapes/qquickshapecurverenderer.cpp
@@ -20,14 +20,8 @@ QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcShapeCurveRenderer, "qt.shape.curverenderer");
-#if !defined(QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN)
-# define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN (1.0f / 32.0f)
-#endif
-
namespace {
-
-
class QQuickShapeWireFrameMaterialShader : public QSGMaterialShader
{
public:
@@ -118,617 +112,6 @@ protected:
};
}
-QVector2D QuadPath::Element::pointAtFraction(float t) const
-{
- if (isLine()) {
- return sp + t * (ep - sp);
- } else {
- const float r = 1 - t;
- return (r * r * sp) + (2 * t * r * cp) + (t * t * ep);
- }
-}
-
-float QuadPath::Element::extent() const
-{
- // TBD: cache this value if we start using it a lot
- QVector2D min(qMin(sp.x(), ep.x()), qMin(sp.y(), ep.y()));
- QVector2D max(qMax(sp.x(), ep.x()), qMax(sp.y(), ep.y()));
- if (!isLine()) {
- min = QVector2D(qMin(min.x(), cp.x()), qMin(min.y(), cp.y()));
- max = QVector2D(qMax(max.x(), cp.x()), qMax(max.y(), cp.y()));
- }
- return (max - min).length();
-}
-
-// Returns the number of intersections between element and a horizontal line at y.
-// The t values of max 2 intersection(s) are stored in the fractions array
-int QuadPath::Element::intersectionsAtY(float y, float *fractions) const
-{
- const float y0 = startPoint().y() - y;
- const float y1 = controlPoint().y() - y;
- const float y2 = endPoint().y() - y;
-
- int numRoots = 0;
- const float a = y0 - (2 * y1) + y2;
- if (a) {
- const float b = (y1 * y1) - (y0 * y2);
- if (b >= 0) {
- const float sqr = qSqrt(b);
- const float root1 = -(-y0 + y1 + sqr) / a;
- if (qIsFinite(root1) && root1 >= 0 && root1 <= 1)
- fractions[numRoots++] = root1;
- const float root2 = (y0 - y1 + sqr) / a;
- if (qIsFinite(root2) && root2 != root1 && root2 >= 0 && root2 <= 1)
- fractions[numRoots++] = root2;
- }
- } else if (y1 != y2) {
- const float root1 = (y2 - (2 * y1)) / (2 * (y2 - y1));
- if (qIsFinite(root1) && root1 >= 0 && root1 <= 1)
- fractions[numRoots++] = root1;
- }
-
- return numRoots;
-}
-
-static float crossProduct(const QVector2D &sp, const QVector2D &p, const QVector2D &ep)
-{
- QVector2D v1 = ep - p;
- QVector2D v2 = p - sp;
- return (v2.x() * v1.y()) - (v2.y() * v1.x());
-}
-
-bool QuadPath::isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
-{
- // Use cross product to compare directions of base vector and vector from start to p
- return crossProduct(sp, p, ep) >= 0.0f;
-}
-
-bool QuadPath::isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
-{
- return qFuzzyIsNull(crossProduct(p, sp, ep));
-}
-
-// Assumes sp != ep
-bool QuadPath::isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
-{
- // epsilon is max length of p-to-baseline relative to length of baseline. So 0.01 means that
- // the distance from p to the baseline must be less than 1% of the length of the baseline.
- constexpr float epsilon = 0.01f;
- QVector2D bv = ep - sp;
- float bl2 = QVector2D::dotProduct(bv, bv);
- float t = QVector2D::dotProduct(p - sp, bv) / bl2;
- QVector2D pv = p - (sp + t * bv);
- return (QVector2D::dotProduct(pv, pv) / bl2) < (epsilon * epsilon);
-}
-
-bool QuadPath::isControlPointOnLeft(const QuadPath::Element &element)
-{
- return isPointOnLeft(element.cp, element.sp, element.ep);
-}
-
-// NOTE: it is assumed that subpaths are closed
-bool QuadPath::contains(const QVector2D &pt) const
-{
- // if (!controlPointRect().contains(pt) : good opt when we add cpr caching
- // return false;
-
- int winding_number = 0;
- for (const Element &e : m_elements) {
- int dir = 1;
- float y1 = e.startPoint().y();
- float y2 = e.endPoint().y();
- if (y2 < y1) {
- qSwap(y1, y2);
- dir = -1;
- }
- if (e.m_isLine) {
- if (pt.y() < y1 || pt.y() >= y2 || y1 == y2)
- continue;
- const float t = (pt.y() - e.startPoint().y()) / (e.endPoint().y() - e.startPoint().y());
- const float x = e.startPoint().x() + t * (e.endPoint().x() - e.startPoint().x());
- if (x <= pt.x())
- winding_number += dir;
- } else {
- y1 = qMin(y1, e.controlPoint().y());
- y2 = qMax(y2, e.controlPoint().y());
- if (pt.y() < y1 || pt.y() >= y2)
- continue;
- float ts[2];
- const int numRoots = e.intersectionsAtY(pt.y(), ts);
- // Count if there is exactly one intersection to the left
- bool oneHit = false;
- float tForHit = -1;
- for (int i = 0; i < numRoots; i++) {
- if (e.pointAtFraction(ts[i]).x() <= pt.x()) {
- oneHit = !oneHit;
- tForHit = ts[i];
- }
- }
- if (oneHit) {
- dir = e.tangentAtFraction(tForHit).y() < 0 ? -1 : 1;
- winding_number += dir;
- }
- }
- };
-
- return (fillRule() == Qt::WindingFill ? (winding_number != 0) : ((winding_number % 2) != 0));
-}
-
-void QuadPath::addElement(const QVector2D &control, const QVector2D &endPoint, bool isLine)
-{
- if (qFuzzyCompare(currentPoint, endPoint))
- return; // 0 length element, skip
-
- isLine = isLine || isPointNearLine(control, currentPoint, endPoint); // Turn flat quad into line
-
- m_elements.resize(m_elements.size() + 1);
- Element &elem = m_elements.last();
- elem.sp = currentPoint;
- elem.cp = isLine ? (0.5f * (currentPoint + endPoint)) : control;
- elem.ep = endPoint;
- elem.m_isLine = isLine;
- elem.m_isSubpathStart = subPathToStart;
- subPathToStart = false;
- currentPoint = endPoint;
-}
-
-QuadPath::Element::CurvatureFlags QuadPath::coordinateOrderOfElement(const QuadPath::Element &element) const
-{
- QVector2D baseLine = element.endPoint() - element.startPoint();
- QVector2D midPoint = element.midPoint();
- // At the midpoint, the tangent of a quad is parallel to the baseline
- QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
- float delta = qMin(element.extent() / 100, QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN);
- QVector2D justRightOfMid = midPoint + (normal * delta);
- bool pathContainsPoint = contains(justRightOfMid);
- return pathContainsPoint ? Element::FillOnRight : Element::CurvatureFlags(0);
-}
-
-QVector2D QuadPath::closestPointOnLine(const QVector2D &start,
- const QVector2D &end,
- const QVector2D &p)
-{
- QVector2D line = end - start;
- float t = QVector2D::dotProduct(p - start, line) / QVector2D::dotProduct(line, line);
- return start + qBound(0.0f, t, 1.0f) * line;
-}
-
-QuadPath QuadPath::fromPainterPath(const QPainterPath &path)
-{
- QuadPath res;
- res.reserve(path.elementCount());
- res.setFillRule(path.fillRule());
-
- QPolygonF quads;
- QPointF sp;
- for (int i = 0; i < path.elementCount(); ++i) {
- QPainterPath::Element element = path.elementAt(i);
-
- QPointF ep(element);
- switch (element.type) {
- case QPainterPath::MoveToElement:
- res.moveTo(QVector2D(ep));
- break;
- case QPainterPath::LineToElement:
- res.lineTo(QVector2D(ep));
- break;
- case QPainterPath::CurveToElement: {
- QPointF cp1 = ep;
- QPointF cp2(path.elementAt(++i));
- ep = path.elementAt(++i);
- QBezier b = QBezier::fromPoints(sp, cp1, cp2, ep);
-#ifndef USE_TOQUADRATICS_IN_QBEZIER
- qt_toQuadratics(b, &quads);
-#else
- quads = b.toQuadratics();
-#endif
- for (int i = 1; i < quads.size(); i += 2) {
- QVector2D cp(quads[i]);
- QVector2D ep(quads[i + 1]);
- res.quadTo(cp, ep);
- }
- break;
- }
- default:
- Q_UNREACHABLE();
- break;
- }
- sp = ep;
- }
-
- return res;
-}
-
-void QuadPath::addCurvatureData()
-{
- // We use the convention that the inside of a curve is on the *right* side of the
- // direction of the baseline.Thus, as long as this is true: if the control point is
- // on the left side of the baseline, the curve is convex and otherwise it is
- // concave. The paths we get can be arbitrary order, but each subpath will have a
- // consistent order. Therefore, for the first curve element in a subpath, we can
- // determine if the direction already follows the convention or not, and then we
- // can easily detect curvature of all subsequent elements in the subpath.
-
- static bool checkAnomaly = qEnvironmentVariableIntValue("QT_QUICKSHAPES_CHECK_ALL_CURVATURE") != 0;
-
- Element::CurvatureFlags flags = Element::CurvatureUndetermined;
- for (QuadPath::Element &element : m_elements) {
- Q_ASSERT(element.childCount() == 0);
- if (element.isSubpathStart()) {
- flags = coordinateOrderOfElement(element);
- } else if (checkAnomaly) {
- Element::CurvatureFlags newFlags = coordinateOrderOfElement(element);
- if (flags != newFlags) {
- qDebug() << "Curvature anomaly detected:" << element
- << "Subpath fill on right:" << (flags & Element::FillOnRight)
- << "Element fill on right:" << (newFlags & Element::FillOnRight);
- flags = newFlags;
- }
- }
-
- if (element.isLine()) {
- element.m_curvatureFlags = flags;
-
- // Set the control point to an arbitrary point on the inside side of the line
- // (doesn't need to actually be inside the shape: it just makes our calculations
- // easier later if it is at the same side as the fill).
- const QVector2D &sp = element.sp;
- const QVector2D &ep = element.ep;
- QVector2D v = ep - sp;
- element.cp = flags & Element::FillOnRight ? sp + QVector2D(-v.y(), v.x()) : sp + QVector2D(v.y(), -v.x());
- } else {
- bool controlPointOnLeft = isControlPointOnLeft(element);
- bool isFillOnRight = flags & Element::FillOnRight;
- bool isConvex = controlPointOnLeft == isFillOnRight;
-
- if (isConvex)
- element.m_curvatureFlags = Element::CurvatureFlags(flags | Element::Convex);
- else
- element.m_curvatureFlags = flags;
- }
- }
-}
-
-QRectF QuadPath::controlPointRect() const
-{
- QRectF res;
- if (elementCount()) {
- QVector2D min, max;
- min = max = m_elements.constFirst().sp;
- // No need to recurse, as split curve's controlpoints are within the parent curve's
- for (const QuadPath::Element &e : std::as_const(m_elements)) {
- min.setX(std::min({ min.x(), e.sp.x(), e.cp.x(), e.ep.x() }));
- min.setY(std::min({ min.y(), e.sp.y(), e.cp.y(), e.ep.y() }));
- max.setX(std::max({ max.x(), e.sp.x(), e.cp.x(), e.ep.x() }));
- max.setY(std::max({ max.y(), e.sp.y(), e.cp.y(), e.ep.y() }));
- }
- res = QRectF(min.toPointF(), max.toPointF());
- }
- return res;
-}
-
-// Count leaf elements
-qsizetype QuadPath::elementCountRecursive() const
-{
- qsizetype count = 0;
- iterateElements([&](const QuadPath::Element &) { count++; });
- return count;
-}
-
-QPainterPath QuadPath::toPainterPath() const
-{
- // Currently only converts the main, unsplit path; no need for the split path identified
- QPainterPath res;
- res.reserve(elementCount());
- res.setFillRule(fillRule());
- for (const Element &element : m_elements) {
- if (element.m_isSubpathStart)
- res.moveTo(element.startPoint().toPointF());
- if (element.m_isLine)
- res.lineTo(element.endPoint().toPointF());
- else
- res.quadTo(element.controlPoint().toPointF(), element.endPoint().toPointF());
- };
- return res;
-}
-
-// Returns a new path since doing it inline would probably be less efficient
-// (technically changing it from O(n) to O(n^2))
-// Note that this function should be called before splitting any elements,
-// so we can assume that the structure is a list and not a tree
-QuadPath QuadPath::subPathsClosed() const
-{
- QuadPath res = *this;
- res.m_elements = {};
- res.m_elements.reserve(elementCount());
- qsizetype subStart = -1;
- qsizetype prevElement = -1;
- for (qsizetype i = 0; i < elementCount(); i++) {
- const auto &element = m_elements.at(i);
- if (element.m_isSubpathStart) {
- if (subStart >= 0 && m_elements[i - 1].ep != m_elements[subStart].sp) {
- res.currentPoint = m_elements[i - 1].ep;
- res.lineTo(m_elements[subStart].sp);
- auto &endElement = res.m_elements.last();
- endElement.m_isSubpathEnd = true;
- // lineTo() can bail out if the points are too close.
- // In that case, just change the end point to be equal to the start
- // (No need to test because the assignment is a no-op otherwise).
- endElement.ep = m_elements[subStart].sp;
- } else if (prevElement >= 0) {
- res.m_elements[prevElement].m_isSubpathEnd = true;
- }
- subStart = i;
- }
- res.m_elements.append(element);
- prevElement = res.m_elements.size() - 1;
- }
-
- if (subStart >= 0 && m_elements.last().ep != m_elements[subStart].sp) {
- res.currentPoint = m_elements.last().ep;
- res.lineTo(m_elements[subStart].sp);
- }
- if (!res.m_elements.isEmpty()) {
- auto &endElement = res.m_elements.last();
- endElement.m_isSubpathEnd = true;
- endElement.ep = m_elements[subStart].sp;
- }
-
- // ### Workaround for triangulator issue: Avoid 3-element paths
- if (res.elementCount() == 3) {
- res.splitElementAt(2);
- res = res.flattened();
- Q_ASSERT(res.elementCount() == 4);
- }
-
- return res;
-}
-
-QuadPath QuadPath::flattened() const
-{
- QuadPath res;
- res.reserve(elementCountRecursive());
- iterateElements([&](const QuadPath::Element &element) { res.m_elements.append(element); });
- return res;
-}
-
-class ElementCutter
-{
-public:
- ElementCutter(const QuadPath::Element &element)
- : m_element(element)
- {
- m_currentPoint = m_element.startPoint();
- if (m_element.isLine())
- m_lineLength = (m_element.endPoint() - m_element.startPoint()).length();
- else
- fillLUT();
- }
-
- bool consume(float length)
- {
- m_lastT = m_currentT;
- m_lastPoint = m_currentPoint;
- float nextBreak = m_consumed + length;
- float breakT = m_element.isLine() ? nextBreak / m_lineLength : tForLength(nextBreak);
- if (breakT < 1) {
- m_currentT = breakT;
- m_currentPoint = m_element.pointAtFraction(m_currentT);
- m_consumed = nextBreak;
- return true;
- } else {
- m_currentT = 1;
- m_currentPoint = m_element.endPoint();
- return false;
- }
- }
-
- QVector2D currentCutPoint()
- {
- return m_currentPoint;
- }
-
- QVector2D currentControlPoint()
- {
- Q_ASSERT(!m_element.isLine());
- // Split curve right at lastT, yields { lastPoint, rcp, endPoint } quad segment
- QVector2D rcp = (1 - m_lastT) * m_element.controlPoint() + m_lastT * m_element.endPoint();
- // Split that left at currentT, yields { lastPoint, lcp, currentPoint } quad segment
- float segmentT = (m_currentT - m_lastT) / (1 - m_lastT);
- QVector2D lcp = (1 - segmentT) * m_lastPoint + segmentT * rcp;
- return lcp;
- }
-
- float lastLength()
- {
- float elemLength = m_element.isLine() ? m_lineLength : m_lut.last();
- return elemLength - m_consumed;
- }
-
-private:
- void fillLUT()
- {
- Q_ASSERT(!m_element.isLine());
- QVector2D ap = m_element.startPoint() - 2 * m_element.controlPoint() + m_element.endPoint();
- QVector2D bp = 2 * m_element.controlPoint() - 2 * m_element.startPoint();
- float A = 4 * QVector2D::dotProduct(ap, ap);
- float B = 4 * QVector2D::dotProduct(ap, bp);
- float C = QVector2D::dotProduct(bp, bp);
- float b = B / (2 * A);
- float c = C / A;
- float k = c - (b * b);
- float l2 = b * std::sqrt(b * b + k);
- float lnom = b + std::sqrt(b * b + k);
- float l0 = 0.5f * std::sqrt(A);
-
- m_lut.resize(LUTSize, 0);
- for (int i = 1; i < LUTSize; i++) {
- float t = float(i) / (LUTSize - 1);
- float u = t + b;
- float w = std::sqrt(u * u + k);
- float l1 = u * w;
- float lden = u + w;
- float l3 = k * std::log(std::fabs(lden / lnom));
- float res = l0 * (l1 - l2 + l3);
- m_lut[i] = res;
- }
- }
-
- float tForLength(float length)
- {
- Q_ASSERT(!m_element.isLine());
- Q_ASSERT(!m_lut.isEmpty());
-
- float res = 2; // I.e. invalid, outside [0, 1] range
- auto it = std::upper_bound(m_lut.cbegin(), m_lut.cend(), length);
- if (it != m_lut.cend()) {
- float nextLength = *it--;
- float prevLength = *it;
- int prevIndex = std::distance(m_lut.cbegin(), it);
- float fraction = (length - prevLength) / (nextLength - prevLength);
- res = (prevIndex + fraction) / (LUTSize - 1);
- }
- return res;
- }
-
- const QuadPath::Element &m_element;
- float m_lastT = 0;
- float m_currentT = 0;
- QVector2D m_lastPoint;
- QVector2D m_currentPoint;
- float m_consumed = 0;
- // For line elements:
- float m_lineLength;
- // For quadratic curve elements:
- static constexpr int LUTSize = 21;
- QVarLengthArray<float, LUTSize> m_lut;
-};
-
-QuadPath QuadPath::dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset) const
-{
- QVarLengthArray<float, 16> pattern;
- float patternLength = 0;
- for (int i = 0; i < 2 * (dashPattern.length() / 2); i++) {
- float dashLength = qMax(lineWidth * dashPattern[i], qreal(0));
- pattern.append(dashLength);
- patternLength += dashLength;
- }
- if (patternLength == 0)
- return {};
-
- int startIndex = 0;
- float startOffset = std::fmod(lineWidth * dashOffset, patternLength);
- if (startOffset < 0)
- startOffset += patternLength;
- for (float dashLength : pattern) {
- if (dashLength > startOffset)
- break;
- startIndex++;
- startOffset -= dashLength;
- }
-
- int dashIndex = startIndex;
- float offset = startOffset;
- QuadPath res;
- for (int i = 0; i < elementCount(); i++) {
- const Element &element = elementAt(i);
- if (element.isSubpathStart()) {
- res.moveTo(element.startPoint());
- dashIndex = startIndex;
- offset = startOffset;
- }
- ElementCutter cutter(element);
- while (true) {
- bool gotAll = cutter.consume(pattern.at(dashIndex) - offset);
- QVector2D nextPoint = cutter.currentCutPoint();
- if (dashIndex & 1)
- res.moveTo(nextPoint); // gap
- else if (element.isLine())
- res.lineTo(nextPoint); // dash in line
- else
- res.quadTo(cutter.currentControlPoint(), nextPoint); // dash in curve
- if (gotAll) {
- offset = 0;
- dashIndex = (dashIndex + 1) % pattern.size();
- } else {
- offset += cutter.lastLength();
- break;
- }
- }
- }
- return res;
-}
-
-void QuadPath::splitElementAt(qsizetype index)
-{
- const qsizetype newChildIndex = m_childElements.size();
- m_childElements.resize(newChildIndex + 2);
- Element &parent = elementAt(index);
- parent.m_numChildren = 2;
- parent.m_firstChildIndex = newChildIndex;
-
- Element &quad1 = m_childElements[newChildIndex];
- const QVector2D mp = parent.midPoint();
- quad1.sp = parent.sp;
- quad1.cp = 0.5f * (parent.sp + parent.cp);
- quad1.ep = mp;
- quad1.m_isSubpathStart = parent.m_isSubpathStart;
- quad1.m_isSubpathEnd = false;
- quad1.m_curvatureFlags = parent.m_curvatureFlags;
- quad1.m_isLine = parent.m_isLine; //### || isPointNearLine(quad1.cp, quad1.sp, quad1.ep);
-
- Element &quad2 = m_childElements[newChildIndex + 1];
- quad2.sp = mp;
- quad2.cp = 0.5f * (parent.ep + parent.cp);
- quad2.ep = parent.ep;
- quad2.m_isSubpathStart = false;
- quad2.m_isSubpathEnd = parent.m_isSubpathEnd;
- quad2.m_curvatureFlags = parent.m_curvatureFlags;
- quad2.m_isLine = parent.m_isLine; //### || isPointNearLine(quad2.cp, quad2.sp, quad2.ep);
-
-#ifndef QT_NO_DEBUG
- if (qFuzzyCompare(quad1.sp, quad1.ep) || qFuzzyCompare(quad2.sp, quad2.ep))
- qDebug() << "###FIXME: quad splitting has yielded ~null quad.";
-#endif
-}
-
-static void printElement(QDebug stream, const QuadPath::Element &element)
-{
- auto printPoint = [&](QVector2D p) { stream << "(" << p.x() << ", " << p.y() << ") "; };
- stream << "{ ";
- printPoint(element.startPoint());
- printPoint(element.controlPoint());
- printPoint(element.endPoint());
- stream << "} " << (element.isLine() ? "L " : "C ") << (element.isConvex() ? "X " : "O ")
- << (element.isSubpathStart() ? "S" : element.isSubpathEnd() ? "E" : "");
-}
-
-QDebug operator<<(QDebug stream, const QuadPath::Element &element)
-{
- QDebugStateSaver saver(stream);
- stream.nospace();
- stream << "QuadPath::Element( ";
- printElement(stream, element);
- stream << " )";
- return stream;
-}
-
-QDebug operator<<(QDebug stream, const QuadPath &path)
-{
- QDebugStateSaver saver(stream);
- stream.nospace();
- stream << "QuadPath(" << path.elementCount() << " main elements, "
- << path.elementCountRecursive() << " leaf elements, "
- << (path.fillRule() == Qt::OddEvenFill ? "OddEven" : "Winding") << Qt::endl;
- qsizetype count = 0;
- path.iterateElements([&](const QuadPath::Element &e) {
- stream << " " << count++ << (e.isSubpathStart() ? " >" : " ");
- printElement(stream, e);
- stream << Qt::endl;
- });
- stream << ")";
- return stream;
-}
QQuickShapeCurveRenderer::~QQuickShapeCurveRenderer() { }
@@ -880,9 +263,9 @@ void QQuickShapeCurveRenderer::updateNode()
if (dirtyFlags & PathDirty) {
if (simplifyPath)
- pathData.path = QuadPath::fromPainterPath(pathData.originalPath.simplified());
+ pathData.path = QQuadPath::fromPainterPath(pathData.originalPath.simplified());
else
- pathData.path = QuadPath::fromPainterPath(pathData.originalPath);
+ pathData.path = QQuadPath::fromPainterPath(pathData.originalPath);
pathData.path.setFillRule(pathData.fillRule);
pathData.fillPath = {};
dirtyFlags |= (FillDirty | StrokeDirty);
@@ -935,6 +318,7 @@ void QQuickShapeCurveRenderer::updateNode()
}
}
+namespace {
// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space.
// v1 maps to [1,0], v2 maps to [0,1]. p is the point to be mapped to uv in this space (i.e. vector from p0)
static inline QVector2D uvForPoint(QVector2D v1, QVector2D v2, QVector2D p)
@@ -956,13 +340,13 @@ static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVecto
return uvForPoint(v1, v2, p - p0);
}
-QVector3D QuadPath::Element::uvForPoint(QVector2D p) const
+static QVector3D elementUvForPoint(const QQuadPath::Element& e, QVector2D p)
{
- auto uv = curveUv(sp, cp, ep, p);
- if (m_isLine)
+ auto uv = curveUv(e.startPoint(), e.controlPoint(), e.endPoint(), p);
+ if (e.isLine())
return { uv.x(), uv.y(), 0.0f };
else
- return { uv.x(), uv.y(), (m_curvatureFlags & Convex) ? -1.0f : 1.0f };
+ return { uv.x(), uv.y(), e.isConvex() ? -1.0f : 1.0f };
}
static inline QVector2D calcNormalVector(QVector2D a, QVector2D b)
@@ -979,7 +363,7 @@ static inline float testSideOfLineByNormal(QVector2D a, QVector2D n, QVector2D p
};
template<typename Func>
-void iteratePath(const QuadPath &path, int index, Func &&lambda)
+void iteratePath(const QQuadPath &path, int index, Func &&lambda)
{
const auto &element = path.elementAt(index);
if (element.childCount() == 0) {
@@ -989,9 +373,18 @@ void iteratePath(const QuadPath &path, int index, Func &&lambda)
iteratePath(path, element.indexOfChild(i), lambda);
}
}
-QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData &pathData, NodeList *debugNodes)
+
+static inline float determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
+{
+ return p1.x() * (p2.y() - p3.y())
+ + p2.x() * (p3.y() - p1.y())
+ + p3.x() * (p1.y() - p2.y());
+}
+}
+
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData &pathData,
+ NodeList *debugNodes)
{
- //qDebug() << "========= STARTING ===========" << pathData.path;
auto *node = new QQuickShapeCurveNode;
node->setGradientType(pathData.gradientType);
@@ -1000,16 +393,15 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
QPainterPath internalHull;
internalHull.setFillRule(pathData.fillPath.fillRule());
-
bool visualizeDebug = debugVisualization() & DebugCurves;
const float dbg = visualizeDebug ? 0.5f : 0.0f;
+ node->setDebug(dbg);
QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
QHash<QPair<float, float>, int> linePointHash;
QHash<QPair<float, float>, int> concaveControlPointHash;
QHash<QPair<float, float>, int> convexPointHash;
-
auto toRoundedPair = [](const QPointF &p) -> QPair<float, float> {
return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f);
};
@@ -1022,21 +414,12 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f };
};
- auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp) {
- float r = 0.0f, g = 0.0f, b = 0.0f;
- if (element.isLine()) {
- g = b = 1.0f;
- } else if (element.isConvex()) {
- r = 1.0;
- } else { // concave
- b = 1.0;
- }
-
- QVector4D dbgColor(r, g, b, dbg);
-
+ auto addCurveTriangle = [&](const QQuadPath::Element &element,
+ const QVector2D &sp,
+ const QVector2D &ep,
+ const QVector2D &cp) {
node->appendTriangle(sp, cp, ep,
- [&element](QVector2D v) { return element.uvForPoint(v); },
- dbgColor, dbgColor, dbgColor);
+ [&element](QVector2D v) { return elementUvForPoint(element, v); });
wfVertices.append({sp.x(), sp.y(), 1.0f, 0.0f, 0.0f}); // 0
wfVertices.append({cp.x(), cp.y(), 0.0f, 1.0f, 0.0f}); // 1
@@ -1044,11 +427,12 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
};
// Find a point on the other side of the line
- auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){
+ auto findPointOtherSide = [](const QVector2D &startPoint,
+ const QVector2D &endPoint,
+ const QVector2D &referencePoint) {
QVector2D baseLine = endPoint - startPoint;
QVector2D insideVector = referencePoint - startPoint;
- //QVector2D midPoint = (endPoint + startPoint) / 2; // ??? Should we use midPoint instead of startPoint??
QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()); // TODO: limit size of triangle
bool swap = QVector2D::dotProduct(insideVector, normal) < 0;
@@ -1056,42 +440,51 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
return swap ? startPoint + normal : startPoint - normal;
};
- auto addLineTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ auto addLineTriangle = [&](const QQuadPath::Element &element,
+ const QVector2D &sp,
+ const QVector2D &ep,
+ const QVector2D &cp) {
addCurveTriangle(element, sp, ep, cp);
// Add a triangle on the outer side of the line to get some more AA
// The new point replaces cp
- QVector2D op = findPointOtherSide(sp, ep, cp); //sp - (cp - ep);
+ QVector2D op = findPointOtherSide(sp, ep, cp);
addCurveTriangle(element, sp, op, ep);
};
- auto addConvexTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ auto addConvexTriangle = [&](const QQuadPath::Element &element,
+ const QVector2D &sp,
+ const QVector2D &ep,
+ const QVector2D &cp) {
addCurveTriangle(element, sp, ep, cp);
// Add two triangles on the outer side to get some more AA
// First triangle on the line sp-cp, replacing ep
{
- QVector2D op = findPointOtherSide(sp, cp, ep); //sp - (ep - cp);
+ QVector2D op = findPointOtherSide(sp, cp, ep);
addCurveTriangle(element, sp, cp, op);
}
// Second triangle on the line ep-cp, replacing sp
{
- QVector2D op = findPointOtherSide(ep, cp, sp); //ep - (sp - cp);
+ QVector2D op = findPointOtherSide(ep, cp, sp);
addCurveTriangle(element, op, cp, ep);
}
-
};
-
// This is guaranteed to be in safe space (the curve will never enter the triangle)
// ### This is the opposite of what we really want: it's going to be extremely thin when we need it,
// and big when it's completely pointless, but a thicker triangle could be going into negative space
- auto oppositePoint = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &controlPoint) -> QVector2D {
+ auto oppositePoint = [](const QVector2D &startPoint,
+ const QVector2D &endPoint,
+ const QVector2D &controlPoint) -> QVector2D {
return startPoint + 2 * (endPoint - controlPoint);
};
// Identical to addLineTriangle, except for how op is calculated
- auto addConcaveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ auto addConcaveTriangle = [&](const QQuadPath::Element &element,
+ const QVector2D &sp,
+ const QVector2D &ep,
+ const QVector2D &cp) {
addCurveTriangle(element, sp, ep, cp);
// Add an outer triangle to give extra AA for very flat curves
QVector2D op = oppositePoint(sp, ep, cp);
@@ -1099,22 +492,17 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
addCurveTriangle(element, sp, op, ep);
};
- auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3){
+ auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3) {
constexpr QVector3D uv(0.0, 1.0, -1.0);
- QVector4D dbgColor(0.0f, 1.0f, 0.0f, dbg);
- node->appendTriangle(p1, p2, p3,
- [&uv](QVector2D) { return uv; },
- dbgColor, dbgColor, dbgColor);
+ node->appendTriangle(p1, p2, p3, [&uv](QVector2D) { return uv; } );
wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0
wfVertices.append({p3.x(), p3.y(), 0.0f, 1.0f, 0.0f}); // 1
wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f}); // 2
};
-
-
- for (int i = 0; i < pathData.fillPath.elementCount(); ++i)
- iteratePath(pathData.fillPath, i, [&](const QuadPath::Element &element, int index){
+ for (int i = 0; i < pathData.fillPath.elementCount(); ++i) {
+ iteratePath(pathData.fillPath, i, [&](const QQuadPath::Element &element, int index) {
QPointF sp(element.startPoint().toPointF()); //### to much conversion to and from pointF
QPointF cp(element.controlPoint().toPointF());
QPointF ep(element.endPoint().toPointF());
@@ -1122,7 +510,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
internalHull.moveTo(sp);
if (element.isLine()) {
internalHull.lineTo(ep);
- //lineSegments.append(QLineF(sp, ep));
linePointHash.insert(toRoundedPair(sp), index);
} else {
if (element.isConvex()) {
@@ -1137,23 +524,24 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
}
}
});
- //qDebug() << "Curves: i" << indices.size() << "v" << vertexBuffer.size() << "w" << wfVertices.size();
+ }
auto makeHashable = [](const QVector2D &p) -> QPair<float, float> {
return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f);
};
// Points in p are already rounded do 1/32
// Returns false if the triangle needs to be split. Adds the triangle to the graphics buffers and returns true otherwise.
-
- // TODO: Does not handle ambiguous vertices that are on multiple unrelated lines/curves
-
- auto onSameSideOfLine = [](const QVector2D &p1, const QVector2D &p2, const QVector2D &linePoint, const QVector2D &lineNormal) {
+ // (Does not handle ambiguous vertices that are on multiple unrelated lines/curves)
+ auto onSameSideOfLine = [](const QVector2D &p1,
+ const QVector2D &p2,
+ const QVector2D &linePoint,
+ const QVector2D &lineNormal) {
float side1 = testSideOfLineByNormal(linePoint, lineNormal, p1);
float side2 = testSideOfLineByNormal(linePoint, lineNormal, p2);
return side1 * side2 >= 0;
};
- auto pointInSafeSpace = [&](const QVector2D &p, const QuadPath::Element &element) -> bool {
+ auto pointInSafeSpace = [&](const QVector2D &p, const QQuadPath::Element &element) -> bool {
const QVector2D a = element.startPoint();
const QVector2D b = element.endPoint();
const QVector2D c = element.controlPoint();
@@ -1180,7 +568,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
if (auto found = linePointHash.constFind(makeHashable(p[i])); found != linePointHash.constEnd()) {
// check if this triangle is on a line, i.e. if one point is the sp and another is the ep of the same path element
const auto &element = pathData.fillPath.elementAt(*found);
- //qDebug() << " " << element;
for (int j = 0; j < 3; ++j) {
if (i != j && roundVec2D(element.endPoint()) == p[j]) {
if (foundElement)
@@ -1188,7 +575,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
lineElementIndex = *found;
si = i;
ei = j;
- //qDebug() << "FOUND IT!!!!" << p[i] << p[j] << lineElementIndex;
foundElement = true;
}
}
@@ -1205,9 +591,8 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
return false; // More than one edge on path: must split
concaveElementIndex = *found;
// The tangent line is p[i] - p[j]
- si = i; // we may not need these
+ si = i;
ei = j;
- //qDebug() << "FOUND IT!!!!" << p[i] << p[j] << lineElementIndex;
foundElement = true;
}
}
@@ -1221,7 +606,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
convexElementIndex = *found;
si = i;
ei = j;
- //qDebug() << "FOUND IT!!!!" << p[i] << p[j] << convexElementIndex;
foundElement = true;
}
}
@@ -1236,18 +620,17 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
int oi = (6 - si - ei) % 3;
const auto &otherPoint = p[oi];
const auto &element = pathData.fillPath.elementAt(convexElementIndex);
- // We have to test whether the triangle can cross the line TODO: use the toplevel element's safe space
+ // We have to test whether the triangle can cross the line
+ // TODO: use the toplevel element's safe space
bool safeSpace = pointInSafeSpace(otherPoint, element);
if (safeSpace) {
addCurveTriangle(element, p[0], p[1], p[2]);
} else {
- //qDebug() << "Point" << otherPoint << "element" << element << "safe" << safeSpace;
// Find a point inside the triangle that's also in the safe space
QVector2D newPoint = (p[0] + p[1] + p[2]) / 3;
// We should calculate the point directly, but just do a lazy implementation for now:
for (int i = 0; i < 7; ++i) {
safeSpace = pointInSafeSpace(newPoint, element);
- //qDebug() << "UNSAFE space round" << i << "checking" << newPoint << safeSpace;
if (safeSpace)
break;
newPoint = (p[si] + p[ei] + newPoint) / 3;
@@ -1279,15 +662,16 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
QVector2D p[3];
for (int i = 0; i < 3; ++i) {
- p[i] = toRoundedVec2D(QPointF(triangles.vertices.at(idx[i]*2), triangles.vertices.at(idx[i]*2 + 1)));
+ p[i] = toRoundedVec2D(QPointF(triangles.vertices.at(idx[i] * 2),
+ triangles.vertices.at(idx[i] * 2 + 1)));
}
+ if (qFuzzyIsNull(determinant(p[0], p[1], p[2])))
+ continue; // Skip degenerate triangles
bool needsSplit = !handleTriangle(p);
if (needsSplit) {
QVector2D c = (p[0] + p[1] + p[2]) / 3;
- //qDebug() << "Split!!! New point" << c;
for (int i = 0; i < 3; ++i) {
qSwap(c, p[i]);
- //qDebug() << "Adding split triangle" << p[0] << p[1] << p[2];
handleTriangle(p);
qSwap(c, p[i]);
}
@@ -1304,13 +688,9 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData
ret.append(node);
}
-
const bool wireFrame = debugVisualization() & DebugWireframe;
if (wireFrame) {
QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
-
- //QVarLengthArray<quint32> indices;
-
QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
wfVertices.size(),
indices.size(),
@@ -1337,8 +717,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNode
QVector<QSGGeometryNode *> ret;
const QColor &color = pathData.pen.color();
- bool visualizeDebug = debugVisualization() & DebugCurves;
- const float dbg = visualizeDebug ? 0.5f : 0.0f;
QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
QTriangulatingStroker stroker;
@@ -1354,7 +732,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNode
QVector2D baseLine = endPoint - startPoint;
QVector2D insideVector = referencePoint - startPoint;
- //QVector2D midPoint = (endPoint + startPoint) / 2; // ??? Should we use midPoint instead of startPoint??
QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()); // TODO: limit size of triangle
bool swap = QVector2D::dotProduct(insideVector, normal) < 0;
@@ -1365,11 +742,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNode
static bool disableExtraTriangles = qEnvironmentVariableIntValue("QT_QUICKSHAPES_WIP_DISABLE_EXTRA_STROKE_TRIANGLES");
auto addStrokeTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, bool){
- //constexpr QVector3D uv(0.0, 1.0, -1.0);
-
-
if (p1 == p2 || p2 == p3) {
- //qDebug() << "Skipping triangle" << p1 << p2 << p3;
return;
}
@@ -1378,11 +751,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNode
return QVector3D(uv.x(), uv.y(), 0.0f); // Line
};
- node->appendTriangle(p1, p2, p3,
- uvForPoint,
- QVector4D(1.0f, 0.0, 0.0, dbg),
- QVector4D(0.0f, 1.0, 0.0, dbg),
- QVector4D(0.0f, 0.0, 1.0, dbg));
+ node->appendTriangle(p1, p2, p3, uvForPoint);
wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0
@@ -1393,11 +762,7 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNode
// Add a triangle on the outer side of the line to get some more AA
// The new point replaces p2 (currentVertex+1)
QVector2D op = findPointOtherSide(p1, p3, p2);
- node->appendTriangle(p1, op, p3,
- uvForPoint,
- QVector4D(1.0f, 0.0, 0.0, dbg),
- QVector4D(1.0f, 1.0, 0.0, dbg),
- QVector4D(0.0f, 0.0, 1.0, dbg));
+ 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
@@ -1428,9 +793,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNode
const bool wireFrame = debugVisualization() & DebugWireframe;
if (wireFrame) {
QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
-
- //QVarLengthArray<quint32> indices;
-
QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
wfVertices.size(),
indices.size(),
@@ -1490,21 +852,22 @@ namespace {
If you can find such an edge then it means that the triangles do not intersect otherwise the
triangles are colliding.
*/
+using TrianglePoints = std::array<QVector2D, 3>;
+using LinePoints = std::array<QVector2D, 2>;
// The sign of the determinant tells the winding order: positive means counter-clockwise
-static inline double determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
+
+static inline double determinant(const TrianglePoints &p)
{
- return p1.x() * (p2.y() - p3.y())
- + p2.x() * (p3.y() - p1.y())
- + p3.x() * (p1.y() - p2.y());
+ return determinant(p[0], p[1], p[2]);
}
// Fix the triangle so that the determinant is positive
-static void fixWinding(QVector2D &p1, QVector2D &p2, QVector2D &p3)
+static void fixWinding(TrianglePoints &p)
{
- double det = determinant(p1, p2, p3);
+ double det = determinant(p);
if (det < 0.0) {
- qSwap(p1, p2);
+ qSwap(p[0], p[1]);
}
}
@@ -1516,10 +879,11 @@ bool checkEdge(QVector2D &p1, QVector2D &p2, QVector2D &p, float epsilon)
return determinant(p1, p2, p) <= epsilon;
}
-bool checkTriangleOverlap(QVector2D *triangle1, QVector2D *triangle2, float epsilon = 1.0/32)
+
+bool checkTriangleOverlap(TrianglePoints &triangle1, TrianglePoints &triangle2, float epsilon = 1.0/32)
{
// See if there is an edge of triangle1 such that all vertices in triangle2 are on the opposite side
- fixWinding(triangle1[0], triangle1[1], triangle1[2]);
+ fixWinding(triangle1);
for (int i = 0; i < 3; i++) {
int ni = (i + 1) % 3;
if (checkEdge(triangle1[i], triangle1[ni], triangle2[0], epsilon) &&
@@ -1529,7 +893,7 @@ bool checkTriangleOverlap(QVector2D *triangle1, QVector2D *triangle2, float epsi
}
// See if there is an edge of triangle2 such that all vertices in triangle1 are on the opposite side
- fixWinding(triangle2[0], triangle2[1], triangle2[2]);
+ fixWinding(triangle2);
for (int i = 0; i < 3; i++) {
int ni = (i + 1) % 3;
@@ -1542,7 +906,7 @@ bool checkTriangleOverlap(QVector2D *triangle1, QVector2D *triangle2, float epsi
return true;
}
-bool checkLineTriangleOverlap(QVector2D *triangle, QVector2D *line, float epsilon = 1.0/32)
+bool checkLineTriangleOverlap(TrianglePoints &triangle, LinePoints &line, float epsilon = 1.0/32)
{
// See if all vertices of the triangle are on the same side of the line
bool s1 = determinant(line[0], line[1], triangle[0]) < 0;
@@ -1553,7 +917,7 @@ bool checkLineTriangleOverlap(QVector2D *triangle, QVector2D *line, float epsilo
return false;
}
// See if there is an edge of triangle1 such that both vertices in line are on the opposite side
- fixWinding(triangle[0], triangle[1], triangle[2]);
+ fixWinding(triangle);
for (int i = 0; i < 3; i++) {
int ni = (i + 1) % 3;
if (checkEdge(triangle[i], triangle[ni], line[0], epsilon) &&
@@ -1572,43 +936,42 @@ bool checkTriangleContains (QVector2D pt, QVector2D v1, QVector2D v2, QVector2D
d2 = determinant(pt, v2, v3);
d3 = determinant(pt, v3, v1);
- bool allNegative = d1 < epsilon && d2 < epsilon && d3 < epsilon;
+ bool allNegative = d1 < -epsilon && d2 < -epsilon && d3 < -epsilon;
bool allPositive = d1 > epsilon && d2 > epsilon && d3 > epsilon;
return allNegative || allPositive;
}
// e1 is always a concave curve. e2 can be curve or line
-static bool isOverlap(const QuadPath &path, qsizetype e1, qsizetype e2)
+static bool isOverlap(const QQuadPath &path, int e1, int e2)
{
- const QuadPath::Element &element1 = path.elementAt(e1);
- const QuadPath::Element &element2 = path.elementAt(e2);
+ const QQuadPath::Element &element1 = path.elementAt(e1);
+ const QQuadPath::Element &element2 = path.elementAt(e2);
- QVector2D t1[3] = { element1.startPoint(), element1.controlPoint(), element1.endPoint() };
+ TrianglePoints t1{ element1.startPoint(), element1.controlPoint(), element1.endPoint() };
if (element2.isLine()) {
- QVector2D line[2] = { element2.startPoint(), element2.endPoint() };
+ LinePoints line{ element2.startPoint(), element2.endPoint() };
return checkLineTriangleOverlap(t1, line);
} else {
- QVector2D t2[3] = { element2.startPoint(), element2.controlPoint(), element2.endPoint() };
+ TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() };
return checkTriangleOverlap(t1, t2);
}
return false;
}
-static bool isOverlap(const QuadPath &path, qsizetype index, const QVector2D &vertex)
+static bool isOverlap(const QQuadPath &path, int index, const QVector2D &vertex)
{
- const QuadPath::Element &elem = path.elementAt(index);
+ const QQuadPath::Element &elem = path.elementAt(index);
return checkTriangleContains(vertex, elem.startPoint(), elem.controlPoint(), elem.endPoint());
}
struct TriangleData
{
- QVector2D points[3];
+ TrianglePoints points;
int pathElementIndex;
- QVector3D debugColor;
- bool specialDebug = false; // Quick way of debugging special cases without having to change debugColor
+ TrianglePoints normals;
};
// Returns a vector that is normal to baseLine, pointing to the right
@@ -1620,7 +983,7 @@ QVector2D normalVector(QVector2D baseLine)
// Returns a vector that is normal to the path and pointing to the right. If endSide is false
// the vector is normal to the start point, otherwise to the end point
-QVector2D normalVector(const QuadPath::Element &element, bool endSide = false)
+QVector2D normalVector(const QQuadPath::Element &element, bool endSide = false)
{
if (element.isLine())
return normalVector(element.endPoint() - element.startPoint());
@@ -1633,7 +996,7 @@ QVector2D normalVector(const QuadPath::Element &element, bool endSide = false)
// Returns a vector that is parallel to the path. If endSide is false
// the vector starts at the start point and points forward,
// otherwise it starts at the end point and points backward
-QVector2D tangentVector(const QuadPath::Element &element, bool endSide = false)
+QVector2D tangentVector(const QQuadPath::Element &element, bool endSide = false)
{
if (element.isLine()) {
if (!endSide)
@@ -1648,78 +1011,34 @@ QVector2D tangentVector(const QuadPath::Element &element, bool endSide = false)
}
}
-
-QVector2D miterBisector(const QuadPath::Element &element1, const QuadPath::Element &element2, float strokeMargin, float inverseMiterLimit,
- bool *ok = nullptr, bool *pointingRight = nullptr)
-{
- Q_ASSERT(element1.endPoint() == element2.startPoint());
-
- const auto p1 = element1.isLine() ? element1.startPoint() : element1.controlPoint();
- const auto p2 = element1.endPoint();
- const auto p3 = element2.isLine() ? element2.endPoint() : element2.controlPoint();
-
- const auto v1 = (p1 - p2).normalized();
- const auto v2 = (p3 - p2).normalized();
- const auto bisector = v1 + v2;
-
- if (qFuzzyIsNull(bisector.x()) && qFuzzyIsNull(bisector.y())) {
- // v1 and v2 are almost parallel, and pointing in opposite directions
- // angle bisector formula will give an almost null vector: use normal of bisector of normals instead
- QVector2D n1(-v1.y(), v1.x());
- QVector2D n2(-v2.y(), v2.x());
- if (ok)
- *ok = true;
- if (pointingRight)
- *pointingRight = true;
- return (n2 - n1).normalized() * strokeMargin;
- } else {
- // We need to increase the length, so that the result covers the whole miter
- // Using the identity sin(x/2) == sqrt((1 - cos(x)) / 2), and the fact that the
- // dot product of two unit vectors is the cosine of the angle between them
- // The length of the miter is w/sin(x/2) where x is the angle between the two elements
-
- float cos2x = QVector2D::dotProduct(v1, v2);
- cos2x = qMin(1.0f, cos2x); // Allow for float inaccuracy
- float sine = sqrt((1.0f - cos2x) / 2);
- if (ok)
- *ok = sine >= inverseMiterLimit / 2.0f;
- if (pointingRight)
- *pointingRight = determinant(p1, p2, p3) > 0;
- sine = qMax(sine, 0.01f); // Avoid divide by zero
- return bisector.normalized() * strokeMargin / sine;
- }
-}
-
// Really simplistic O(n^2) triangulator - only intended for five points
-QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, int elementIndex)
+QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, const QList<QVector2D> &normals, int elementIndex)
{
int count = pts.size();
Q_ASSERT(count >= 3);
- QList<TriangleData> ret;
+ Q_ASSERT(normals.size() == count);
- ret.append({{pts[0], pts[1], pts[2]}, elementIndex, {1, 0, 0}});
-
- // hull is always in positive determinant winding order
- QList<QVector2D> hull;
+ // First we find the convex hull: it's always in positive determinant winding order
+ QList<int> hull;
float det1 = determinant(pts[0], pts[1], pts[2]);
if (det1 > 0)
- hull << pts[0] << pts[1] << pts[2];
+ hull << 0 << 1 << 2;
else
- hull << pts[2] << pts[1] << pts[0];
- auto connectableInHull = [&](const QVector2D &pt) -> QList<int> {
+ hull << 2 << 1 << 0;
+ auto connectableInHull = [&](int idx) -> QList<int> {
QList<int> r;
const int n = hull.size();
+ const auto &pt = pts[idx];
for (int i = 0; i < n; ++i) {
- const auto &p1 = hull.at(i);
- const auto &p2 = hull.at((i+1) % n);
- if (determinant(p1, p2, pt) < 0.0f)
+ const auto &i1 = hull.at(i);
+ const auto &i2 = hull.at((i+1) % n);
+ if (determinant(pts[i1], pts[i2], pt) < 0.0f)
r << i;
}
return r;
};
for (int i = 3; i < count; ++i) {
- const auto &p = pts[i];
- auto visible = connectableInHull(p);
+ auto visible = connectableInHull(i);
if (visible.isEmpty())
continue;
int visCount = visible.count();
@@ -1733,28 +1052,62 @@ QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, int ele
break;
}
}
- // Now add those triangles
- for (int j = 0; j < visCount; ++j) {
- const auto &p1 = hull.at((boundaryStart + j) % hullCount);
- const auto &p2 = hull.at((boundaryStart + j+1) % hullCount);
- ret.append({{p1, p2, p}, elementIndex, {1,1,0}});
- }
// Finally replace the points that are now inside the hull
- // We insert p after boundaryStart, and before boundaryStart + visCount (modulo...)
- // and remove the points inbetween
+ // We insert the new point after boundaryStart, and before boundaryStart + visCount (modulo...)
+ // and remove the points in between
int pointsToKeep = hullCount - visCount + 1;
- QList<QVector2D> newHull;
- newHull << p;
+ QList<int> newHull;
+ newHull << i;
for (int j = 0; j < pointsToKeep; ++j) {
newHull << hull.at((j + boundaryStart + visCount) % hullCount);
}
hull = newHull;
}
+
+ // Now that we have a convex hull, we can trivially triangulate it
+ QList<TriangleData> ret;
+ for (int i = 1; i < hull.size() - 1; ++i) {
+ 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]}});
+ }
return ret;
}
+static bool needsSplit(const QQuadPath::Element &el, float penWidth)
+{
+ Q_UNUSED(penWidth)
+ const auto v1 = el.controlPoint() - el.startPoint();
+ const auto v2 = el.endPoint() - el.controlPoint();
+ float cos = QVector2D::dotProduct(v1, v2) / (v1.length() * v2.length());
+ return cos < 0.9;
+}
+static void splitElementIfNecessary(QQuadPath &path, int index, float penWidth)
+{
+ auto &e = path.elementAt(index);
+ if (e.isLine())
+ return;
+ if (e.childCount() == 0) {
+ if (needsSplit(e, penWidth))
+ path.splitElementAt(index);
+ } else {
+ for (int i = 0; i < e.childCount(); ++i)
+ splitElementIfNecessary(path, e.indexOfChild(i), penWidth);
+ }
+}
+
+static QQuadPath subdivide(const QQuadPath &path, int subdivisions, float penWidth)
+{
+ QQuadPath newPath = path;
+
+ for (int i = 0; i < subdivisions; ++i)
+ for (int j = 0; j < newPath.elementCount(); j++)
+ splitElementIfNecessary(newPath, j, penWidth);
+ return newPath;
+}
-static QList<TriangleData> customTriangulator2(const QuadPath &path, float penWidth, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, float miterLimit)
+static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penWidth, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, float miterLimit)
{
const bool bevelJoin = joinStyle == Qt::BevelJoin;
const bool roundJoin = joinStyle == Qt::RoundJoin;
@@ -1762,16 +1115,147 @@ static QList<TriangleData> customTriangulator2(const QuadPath &path, float penWi
const bool roundCap = capStyle == Qt::RoundCap;
const bool squareCap = capStyle == Qt::SquareCap;
+ // We can't use the simple miter for miter joins, since the shader currently only supports round joins
+ const bool simpleMiter = joinStyle == Qt::RoundJoin;
Q_ASSERT(miterLimit > 0 || !miterJoin);
float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0;
+ const float penFactor = penWidth / 2;
+
+ // Returns {inner1, inner2, outer1, outer2, outerMiter}
+ // where foo1 is for the end of element1 and foo2 is for the start of element2
+ // and inner1 == inner2 unless we had to give up finding a decent point
+ auto calculateJoin = [&](const QQuadPath::Element *element1, const QQuadPath::Element *element2,
+ bool &outerBisectorWithinMiterLimit, bool &innerIsRight, bool &giveUp) -> std::array<QVector2D, 5>
+ {
+ outerBisectorWithinMiterLimit = true;
+ innerIsRight = true;
+ giveUp = false;
+ if (!element1) {
+ Q_ASSERT(element2);
+ QVector2D n = normalVector(*element2).normalized();
+ return {n, n, -n, -n, -n};
+ }
+ if (!element2) {
+ Q_ASSERT(element1);
+ QVector2D n = normalVector(*element1, true).normalized();
+ return {n, n, -n, -n, -n};
+ }
+
+ Q_ASSERT(element1->endPoint() == element2->startPoint());
+
+ const auto p1 = element1->isLine() ? element1->startPoint() : element1->controlPoint();
+ const auto p2 = element1->endPoint();
+ const auto p3 = element2->isLine() ? element2->endPoint() : element2->controlPoint();
+
+ const auto v1 = (p1 - p2).normalized();
+ const auto v2 = (p3 - p2).normalized();
+ const auto b = (v1 + v2);
+
+ constexpr float epsilon = 1.0f / 32.0f;
+ bool smoothJoin = qAbs(b.x()) < epsilon && qAbs(b.y()) < epsilon;
- static const int additionalSpace = qEnvironmentVariableIntValue("QT_QUICKSHAPES_EXTRA_SPACE");
+ if (smoothJoin) {
+ // v1 and v2 are almost parallel and pointing in opposite directions
+ // angle bisector formula will give an almost null vector: use normal of bisector of normals instead
+ QVector2D n1(-v1.y(), v1.x());
+ QVector2D n2(-v2.y(), v2.x());
+ QVector2D n = (n2 - n1).normalized();
+ return {n, n, -n, -n, -n};
+ }
+ // Calculate the length of the bisector, so it will cover the entire miter.
+ // Using the identity sin(x/2) == sqrt((1 - cos(x)) / 2), and the fact that the
+ // dot product of two unit vectors is the cosine of the angle between them
+ // The length of the miter is w/sin(x/2) where x is the angle between the two elements
+
+ const auto bisector = b.normalized();
+ float cos2x = QVector2D::dotProduct(v1, v2);
+ cos2x = qMin(1.0f, cos2x); // Allow for float inaccuracy
+ float sine = sqrt((1.0f - cos2x) / 2);
+ innerIsRight = determinant(p1, p2, p3) > 0;
+ sine = qMax(sine, 0.01f); // Avoid divide by zero
+ float length = penFactor / sine;
+
+ // Check if bisector is longer than one of the lines it's trying to bisect
+
+ auto tooLong = [](QVector2D p1, QVector2D p2, QVector2D n, float length, float margin) -> 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)
+ // (we're adding a 10% safety margin to make room for AA -- not exact)
+ auto projLen = QVector2D::dotProduct(v, n);
+ return projLen * 0.9f < length && (v - n * projLen).length() * 0.9 < margin;
+ };
+
+
+ // The angle bisector of the tangent lines is not correct for curved lines. We could fix this by calculating
+ // the exact intersection point, but for now just give up and use the normals.
- const float extraFactor = roundJoin && additionalSpace ? (penWidth + additionalSpace) / penWidth : 2.0;
+ giveUp = !element1->isLine() || !element2->isLine()
+ || tooLong(p1, p2, bisector, length, penFactor)
+ || tooLong(p3, p2, bisector, length, penFactor);
+ outerBisectorWithinMiterLimit = sine >= inverseMiterLimit / 2.0f;
+ bool simpleJoin = simpleMiter && outerBisectorWithinMiterLimit && !giveUp;
+ const QVector2D bn = bisector / sine;
+
+ if (simpleJoin)
+ return {bn, bn, -bn, -bn, -bn}; // We only have one inner and one outer point TODO: change inner point when conflict/curve
+ const QVector2D n1 = normalVector(*element1, true).normalized();
+ const QVector2D n2 = normalVector(*element2).normalized();
+ if (giveUp) {
+ if (innerIsRight)
+ return {n1, n2, -n1, -n2, -bn};
+ else
+ return {-n1, -n2, n1, n2, -bn};
+
+ } else {
+ if (innerIsRight)
+ return {bn, bn, -n1, -n2, -bn};
+ else
+ return {bn, bn, n1, n2, -bn};
+ }
+ };
QList<TriangleData> ret;
+
+ 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 = path.elementAt(idx);
+ 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).normalized();
+ QVector2D endNormal = normalVector(element, true).normalized();
+ QVector2D controlPointNormal = (startNormal + endNormal).normalized();
+ if (controlPointOnRight)
+ controlPointNormal = -controlPointNormal;
+ QVector2D p5 = c + controlPointNormal * penFactor; // This is too simplistic
+ TrianglePoints t1{p1, p2, p5};
+ TrianglePoints t2{p3, p4, p5};
+ 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}});
+ if (controlPointOnRight) {
+ ret.append({{p1, p3, p5}, idx, {n1, n3, controlPointNormal}});
+ } else {
+ ret.append({{p2, p4, p5}, idx, {n2, n4, controlPointNormal}});
+ }
+ } else {
+ ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx));
+ }
+ };
+
+ // 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.
+
int count = path.elementCount();
int subStart = 0;
while (subStart < count) {
@@ -1790,7 +1274,15 @@ static QList<TriangleData> customTriangulator2(const QuadPath &path, float penWi
bool closed = path.elementAt(subStart).startPoint() == path.elementAt(subEnd).endPoint();
const int subCount = subEnd - subStart + 1;
- auto elementAt = [&](int idx, int delta) -> const QuadPath::Element * {
+ 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;
@@ -1808,124 +1300,122 @@ static QList<TriangleData> customTriangulator2(const QuadPath &path, float penWi
const auto *prevElement = elementAt(i, -1);
const auto &s = element.startPoint();
- const auto &c = element.controlPoint();
const auto &e = element.endPoint();
- // Normals point to the right
- QVector2D startNormal = normalVector(element).normalized() * (penWidth / 2);
- QVector2D endNormal = normalVector(element, true).normalized() * (penWidth / 2);
-
- // Bisectors point to the inside of the curve: make them point the same way as normals
- bool startBisectorPointsRight = true;
- bool startBisectorWithinMiterLimit = true;
- QVector2D startBisector = prevElement ? miterBisector(*prevElement, element, penWidth / 2, inverseMiterLimit, &startBisectorWithinMiterLimit, &startBisectorPointsRight) : startNormal;
- if (!startBisectorPointsRight)
- startBisector = -startBisector;
-
- bool endBisectorPointsRight = true;
- bool endBisectorWithinMiterLimit = true;
- QVector2D endBisector = nextElement ? miterBisector(element, *nextElement, penWidth / 2, inverseMiterLimit, &endBisectorWithinMiterLimit, &endBisectorPointsRight) : endNormal;
- if (!endBisectorPointsRight)
- endBisector = -endBisector;
-
- // We can't use the simple miter for miter joins, since the shader currently only supports round joins
- bool simpleMiter = joinStyle == Qt::RoundJoin;
-
- // TODO: miterLimit
- bool startMiter = simpleMiter && startBisectorWithinMiterLimit;
- bool endMiter = simpleMiter && endBisectorWithinMiterLimit;
+ bool startInnerIsRight;
+ bool startBisectorWithinMiterLimit; // Not used
+ bool giveUpOnStartJoin; // Not used
+ auto startJoin = calculateJoin(prevElement, &element,
+ startBisectorWithinMiterLimit, startInnerIsRight,
+ giveUpOnStartJoin);
+ const QVector2D &startInner = startJoin[1];
+ const QVector2D &startOuter = startJoin[3];
+
+ bool endInnerIsRight;
+ bool endBisectorWithinMiterLimit;
+ bool giveUpOnEndJoin;
+ auto endJoin = calculateJoin(&element, nextElement,
+ endBisectorWithinMiterLimit, endInnerIsRight,
+ giveUpOnEndJoin);
+ QVector2D endInner = endJoin[0];
+ QVector2D endOuter = endJoin[2];
+ QVector2D nextOuter = endJoin[3];
+ QVector2D outerB = endJoin[4];
QVector2D p1, p2, p3, p4;
- if (startMiter) {
- //### bisector on inside can extend further than element: must limit it somehow
- p1 = s + startBisector * extraFactor;
- p2 = s - startBisector * extraFactor;
- } else {
- // TODO: remove the overdraw by using the intersection point on the inside (for lines, this is the bisector, but
- // it's more complex for non-smooth curve joins)
+ QVector2D n1, n2, n3, n4;
- // For now, simple bevel: just use normals and live with overdraw
- p1 = s + startNormal * extraFactor;
- p2 = s - startNormal * extraFactor;
+ if (startInnerIsRight) {
+ n1 = startInner;
+ n2 = startOuter;
+ } else {
+ n1 = startOuter;
+ n2 = startInner;
}
+
+ p1 = s + n1 * penFactor;
+ p2 = s + n2 * penFactor;
+
// repeat logic above for the other end:
- if (endMiter) {
- p3 = e + endBisector * extraFactor;
- p4 = e - endBisector * extraFactor;
+ if (endInnerIsRight) {
+ n3 = endInner;
+ n4 = endOuter;
} else {
- p3 = e + endNormal * extraFactor;
- p4 = e - endNormal * extraFactor;
+ n3 = endOuter;
+ n4 = endInner;
}
+ p3 = e + n3 * penFactor;
+ p4 = e + n4 * penFactor;
+
// End caps
if (!prevElement) {
- QVector2D capSpace = tangentVector(element).normalized() * -penWidth;
+ QVector2D capSpace = tangentVector(element).normalized() * -penFactor;
if (roundCap) {
p1 += capSpace;
p2 += capSpace;
} else if (squareCap) {
QVector2D c1 = p1 + capSpace;
QVector2D c2 = p2 + capSpace;
- ret.append({{p1, s, c1}, -1, {1, 1, 0}, true});
- ret.append({{c1, s, c2}, -1, {1, 1, 0}, true});
- ret.append({{p2, s, c2}, -1, {1, 1, 0}, true});
+ ret.append({{p1, s, c1}, -1, {}});
+ ret.append({{c1, s, c2}, -1, {}});
+ ret.append({{p2, s, c2}, -1, {}});
}
}
if (!nextElement) {
- QVector2D capSpace = tangentVector(element, true).normalized() * -penWidth;
+ QVector2D capSpace = tangentVector(element, true).normalized() * -penFactor;
if (roundCap) {
p3 += capSpace;
p4 += capSpace;
} else if (squareCap) {
QVector2D c3 = p3 + capSpace;
QVector2D c4 = p4 + capSpace;
- ret.append({{p3, e, c3}, -1, {1, 1, 0}, true});
- ret.append({{c3, e, c4}, -1, {1, 1, 0}, true});
- ret.append({{p4, e, c4}, -1, {1, 1, 0}, true});
+ 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, {0,1,0}, false});
- ret.append({{p2, p3, p4}, i, {0.5,1,0}, false});
+ ret.append({{p1, p2, p3}, i, {n1, n2, n3}});
+ ret.append({{p2, p3, p4}, i, {n2, n3, n4}});
} else {
- bool controlPointOnRight = determinant(s, c, e) > 0;
- QVector2D controlPointOffset = (startNormal + endNormal).normalized() * penWidth;
- QVector2D p5 = controlPointOnRight ? c - controlPointOffset : c + controlPointOffset;
- ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, i));
+ triangulateCurve(i, p1, p2, p3, p4, n1, n2, n3, n4);
}
- if (!endMiter && nextElement) {
+ bool trivialJoin = simpleMiter && endBisectorWithinMiterLimit && !giveUpOnEndJoin;
+ if (!trivialJoin && nextElement) {
// inside of join (opposite of bevel) is defined by
// triangle s, e, next.e
- QVector2D outer1, outer2;
- const auto &np = nextElement->isLine() ? nextElement->endPoint() : nextElement->controlPoint();
- bool innerOnRight = endBisectorPointsRight;
- auto nextNormal = calcNormalVector(e, np).normalized() * penWidth / 2;
-
- if (innerOnRight) {
- outer1 = e - 2 * endNormal;
- outer2 = e + 2 * nextNormal;
- } else {
- outer1 = e + 2 * endNormal;
- outer2 = e - 2 * nextNormal;
- }
+ bool innerOnRight = endInnerIsRight;
+
+ const auto outer1 = e + endOuter * penFactor;
+ const auto outer2 = e + nextOuter * penFactor;
+ //const auto inner = e + endInner * penFactor;
if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) {
- ret.append({{outer1, e, outer2}, -1, {1,1,0}, false});
+ ret.append({{outer1, e, outer2}, -1, {}});
} else if (roundJoin) {
- ret.append({{outer1, e, outer2}, i, {1,1,0}, false});
- QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penWidth / 2;
+ ret.append({{outer1, e, outer2}, i, {}});
+ QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penFactor;
if (!innerOnRight)
nn = -nn;
- ret.append({{outer1, outer1 + nn, outer2}, i, {1,1,0}, false});
- ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {1,1,0}, false});
+ ret.append({{outer1, outer1 + nn, outer2}, i, {}});
+ ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {}});
} else if (miterJoin) {
- QVector2D outer = innerOnRight ? e - endBisector * 2 : e + endBisector * 2;
- ret.append({{outer1, e, outer}, -2, {1,1,0}, false});
- ret.append({{outer, e, outer2}, -2, {1,1,0}, false});
+ QVector2D outer = e + outerB * penFactor;
+ 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}});
+ // 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}});
}
}
}
@@ -1941,66 +1431,72 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addCurveStrokeNodes(const P
QVector<QSGGeometryNode *> ret;
const QColor &color = pathData.pen.color();
+ const bool debug = debugVisualization() & DebugCurves;
auto *node = new QQuickShapeStrokeNode;
-
+ node->setDebug(0.2f * debug);
QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
float miterLimit = pathData.pen.miterLimit();
float penWidth = pathData.pen.widthF();
- auto thePath = pathData.strokePath;
+ static const int subdivisions = qEnvironmentVariable("QT_QUICKSHAPES_STROKE_SUBDIVISIONS", QStringLiteral("3")).toInt();
+ auto thePath = subdivide(pathData.strokePath, subdivisions, penWidth).flattened(); // TODO: don't flatten, but handle it in the triangulator
auto triangles = customTriangulator2(thePath, penWidth, pathData.pen.joinStyle(), pathData.pen.capStyle(), miterLimit);
- auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &p0, const QVector2D &p1, const QVector2D &p2){
-
+ auto addCurveTriangle = [&](const QQuadPath::Element &element, const TriangleData &t){
+ const QVector2D &p0 = t.points[0];
+ const QVector2D &p1 = t.points[1];
+ const QVector2D &p2 = t.points[2];
if (element.isLine()) {
- node->appendTriangle(p0, p1, p2,
- element.startPoint(), element.endPoint());
+ node->appendTriangle(t.points,
+ LinePoints{element.startPoint(), element.endPoint()},
+ t.normals);
} else {
- node->appendTriangle(p0, p1, p2,
- element.startPoint(), element.controlPoint(), element.endPoint());
+ node->appendTriangle(t.points,
+ { element.startPoint(), element.controlPoint(), element.endPoint()},
+ t.normals);
}
wfVertices.append({p0.x(), p0.y(), 1.0f, 0.0f, 0.0f});
wfVertices.append({p1.x(), p1.y(), 0.0f, 1.0f, 0.0f});
wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f});
};
- // TESTING
- auto addBevelTriangle = [&](const QVector2D &p0, const QVector2D &p1, const QVector2D &p2)
+ auto addBevelTriangle = [&](const TrianglePoints &p)
{
- QVector2D fp1 = p1 + (p0 - p1) / 2;
- QVector2D fp2 = p1 + (p2 - p1) / 2;
- QVector2D fcp = p1; // does not matter for line
+ QVector2D fp1 = p[0];
+ QVector2D fp2 = p[2];
// That describes a path that passes through those points. We want the stroke
// edge, so we need to shift everything down by the stroke offset
- QVector2D nn = calcNormalVector(p0, p2);
- if (determinant(p0, p1, p2) < 0)
+ QVector2D nn = calcNormalVector(p[0], p[2]);
+ if (determinant(p) < 0)
nn = -nn;
float delta = penWidth / 2;
QVector2D offset = nn.normalized() * delta;
fp1 += offset;
fp2 += offset;
- fcp += offset;
- node->appendTriangle(p0, p1, p2, fp1, fp2);
+ TrianglePoints n;
+ // p1 is inside, so n[1] is {0,0}
+ n[0] = (p[0] - p[1]).normalized();
+ n[2] = (p[2] - p[1]).normalized();
- wfVertices.append({p0.x(), p0.y(), 1.0f, 0.0f, 0.0f});
- wfVertices.append({p1.x(), p1.y(), 0.0f, 1.0f, 0.0f});
- wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f});
- //qDebug() << "bev" << p1 << p2 << p3;
+ node->appendTriangle(p, LinePoints{fp1, fp2}, n);
+
+ wfVertices.append({p[0].x(), p[0].y(), 1.0f, 0.0f, 0.0f});
+ wfVertices.append({p[1].x(), p[1].y(), 0.0f, 1.0f, 0.0f});
+ wfVertices.append({p[2].x(), p[2].y(), 0.0f, 0.0f, 1.0f});
};
for (const auto &triangle : triangles) {
if (triangle.pathElementIndex < 0) {
- // TODO: improve bevel logic
- addBevelTriangle(triangle.points[0], triangle.points[1], triangle.points[2]);
+ addBevelTriangle(triangle.points);
continue;
}
const auto &element = thePath.elementAt(triangle.pathElementIndex);
- addCurveTriangle(element, triangle.points[0], triangle.points[1], triangle.points[2]);
+ addCurveTriangle(element, triangle);
}
auto indexCopy = node->uncookedIndexes(); // uncookedIndexes get delete on cooking
@@ -2011,7 +1507,6 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addCurveStrokeNodes(const P
m_rootNode->appendChildNode(node);
ret.append(node);
-
const bool wireFrame = debugVisualization() & DebugWireframe;
if (wireFrame) {
QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
@@ -2034,13 +1529,12 @@ QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addCurveStrokeNodes(const P
debugNodes->append(wfNode);
}
-
return ret;
}
-//TODO: we could optimize by preprocessing e1, since we call this function multiple times on the same
+// TODO: we could optimize by preprocessing e1, since we call this function multiple times on the same
// elements
-static void handleOverlap(QuadPath &path, qsizetype e1, qsizetype e2, int recursionLevel = 0)
+static void handleOverlap(QQuadPath &path, int e1, int e2, int recursionLevel = 0)
{
if (!isOverlap(path, e1, e2)) {
return;
@@ -2052,13 +1546,11 @@ static void handleOverlap(QuadPath &path, qsizetype e1, qsizetype e2, int recurs
}
if (path.elementAt(e1).childCount() > 1) {
- // qDebug() << "Case 1, recursion level" << recursionLevel;
auto e11 = path.indexOfChildAt(e1, 0);
auto e12 = path.indexOfChildAt(e1, 1);
handleOverlap(path, e11, e2, recursionLevel + 1);
handleOverlap(path, e12, e2, recursionLevel + 1);
} else if (path.elementAt(e2).childCount() > 1) {
- // qDebug() << "Case 2, recursion level" << recursionLevel;
auto e21 = path.indexOfChildAt(e2, 0);
auto e22 = path.indexOfChildAt(e2, 1);
handleOverlap(path, e1, e21, recursionLevel + 1);
@@ -2069,9 +1561,6 @@ static void handleOverlap(QuadPath &path, qsizetype e1, qsizetype e2, int recurs
auto e12 = path.indexOfChildAt(e1, 1);
bool overlap1 = isOverlap(path, e11, e2);
bool overlap2 = isOverlap(path, e12, e2);
- // qDebug() << "actual split, recursion level" << recursionLevel << "new overlaps" <<
- // overlap1 << overlap2;
-
if (!overlap1 && !overlap2)
return; // no more overlap: success!
@@ -2100,8 +1589,7 @@ static void handleOverlap(QuadPath &path, qsizetype e1, qsizetype e2, int recurs
}
// Test if element contains a start point of another element
-static void handleOverlap(QuadPath &path, qsizetype e1, const QVector2D vertex,
- int recursionLevel = 0)
+static void handleOverlap(QQuadPath &path, int e1, const QVector2D vertex, int recursionLevel = 0)
{
// First of all: Ignore the next element: it trivially overlaps (maybe not necessary: we do check for strict containment)
if (vertex == path.elementAt(e1).endPoint() || !isOverlap(path, e1, vertex))
@@ -2119,15 +1607,15 @@ static void handleOverlap(QuadPath &path, qsizetype e1, const QVector2D vertex,
handleOverlap(path, path.indexOfChildAt(e1, 1), vertex, recursionLevel + 1);
}
-void QQuickShapeCurveRenderer::solveOverlaps(QuadPath &path)
+void QQuickShapeCurveRenderer::solveOverlaps(QQuadPath &path)
{
- for (qsizetype i = 0; i < path.elementCount(); i++) {
+ for (int i = 0; i < path.elementCount(); i++) {
auto &element = path.elementAt(i);
// only concave curve overlap is problematic, as long as we don't allow self-intersecting curves
if (element.isLine() || element.isConvex())
continue;
- for (qsizetype j = 0; j < path.elementCount(); j++) {
+ for (int j = 0; j < path.elementCount(); j++) {
if (i == j)
continue; // Would be silly to test overlap with self
auto &other = path.elementAt(j);
@@ -2146,12 +1634,12 @@ void QQuickShapeCurveRenderer::solveOverlaps(QuadPath &path)
// We do this in a separate loop, since the triangle/triangle test above is more expensive, and
// if we did this first, there would be more triangles to test
- for (qsizetype i = 0; i < path.elementCount(); i++) {
+ for (int i = 0; i < path.elementCount(); i++) {
auto &element = path.elementAt(i);
if (!element.isConvex())
continue;
- for (qsizetype j = 0; j < path.elementCount(); j++) {
+ for (int j = 0; j < path.elementCount(); j++) {
// We only need to check one point per element, since all subpaths are closed
// Could do smartness to skip elements that cannot overlap, but let's do it the easy way first
if (i == j)
diff --git a/src/quickshapes/qquickshapecurverenderer_p.h b/src/quickshapes/qquickshapecurverenderer_p.h
index 1832d53ec0..92ac8d883b 100644
--- a/src/quickshapes/qquickshapecurverenderer_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p.h
@@ -17,6 +17,7 @@
#include <QtQuickShapes/private/qquickshapesglobal_p.h>
#include <QtQuickShapes/private/qquickshape_p_p.h>
+#include <QtQuickShapes/private/qquadpath_p.h>
#include <qsgnode.h>
#include <qsggeometry.h>
#include <qsgmaterial.h>
@@ -28,235 +29,6 @@
QT_BEGIN_NAMESPACE
-class QuadPath
-{
-public:
- void moveTo(const QVector2D &to)
- {
- subPathToStart = true;
- currentPoint = to;
- }
-
- void lineTo(const QVector2D &to)
- {
- addElement({}, to, true);
- }
-
- void quadTo(const QVector2D &control, const QVector2D &to)
- {
- addElement(control, to);
- }
-
- QRectF controlPointRect() const;
-
- Qt::FillRule fillRule() const { return m_fillRule; }
- void setFillRule(Qt::FillRule rule) { m_fillRule = rule; }
-
- void reserve(qsizetype size) { m_elements.reserve(size); }
- qsizetype elementCount() const { return m_elements.size(); }
- bool isEmpty() const { return m_elements.size() == 0; }
- qsizetype elementCountRecursive() const;
-
- static QuadPath fromPainterPath(const QPainterPath &path);
- QPainterPath toPainterPath() const;
- QuadPath subPathsClosed() const;
- QuadPath flattened() const;
- QuadPath dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset = 0) const;
-
- class Element
- {
- public:
- Element ()
- : m_isSubpathStart(false), m_isSubpathEnd(false), m_isLine(false)
- {
- }
-
- bool isSubpathStart() const
- {
- return m_isSubpathStart;
- }
-
- bool isSubpathEnd() const
- {
- return m_isSubpathEnd;
- }
-
- bool isLine() const
- {
- return m_isLine;
- }
-
- bool isConvex() const
- {
- return m_curvatureFlags & Convex;
- }
-
- QVector2D startPoint() const
- {
- return sp;
- }
-
- QVector2D controlPoint() const
- {
- return cp;
- }
-
- QVector2D endPoint() const
- {
- return ep;
- }
-
- QVector2D midPoint() const
- {
- return isLine() ? 0.5f * (sp + ep) : (0.25f * sp) + (0.5f * cp) + (0.25 * ep);
- }
-
- QVector3D uvForPoint(QVector2D p) const;
-
- std::array<QVector2D, 3> ABC() const;
-
- QVector3D HGForPoint(QVector2D p) const;
-
- qsizetype childCount() const { return m_numChildren; }
-
- qsizetype indexOfChild(qsizetype childNumber) const
- {
- Q_ASSERT(childNumber >= 0 && childNumber < childCount());
- return -(m_firstChildIndex + 1 + childNumber);
- }
-
- QVector2D pointAtFraction(float t) const;
-
- QVector2D tangentAtFraction(float t) const
- {
- return isLine() ? (ep - sp) : ((1 - t) * 2 * (cp - sp)) + (t * 2 * (ep - cp));
- }
-
- QVector2D normalAtFraction(float t) const
- {
- const QVector2D tan = tangentAtFraction(t);
- return QVector2D(-tan.y(), tan.x());
- }
-
- float extent() const;
-
- void setAsConvex(bool isConvex)
- {
- if (isConvex)
- m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags | Element::Convex);
- else
- m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::Convex);
-
- }
-
- private:
- int intersectionsAtY(float y, float *fractions) const;
-
- enum CurvatureFlags : quint8 {
- CurvatureUndetermined = 0,
- FillOnRight = 1,
- Convex = 2
- };
-
- QVector2D sp;
- QVector2D cp;
- QVector2D ep;
- int m_firstChildIndex = 0;
- quint8 m_numChildren = 0;
- CurvatureFlags m_curvatureFlags = CurvatureUndetermined;
- quint8 m_isSubpathStart : 1;
- quint8 m_isSubpathEnd : 1;
- quint8 m_isLine : 1;
- friend class QuadPath;
- friend QDebug operator<<(QDebug, const QuadPath::Element &);
- };
-
- template<typename Func>
- void iterateChildrenOf(Element &e, Func &&lambda)
- {
- const qsizetype lastChildIndex = e.m_firstChildIndex + e.childCount() - 1;
- for (qsizetype i = e.m_firstChildIndex; i <= lastChildIndex; i++) {
- Element &c = m_childElements[i];
- if (c.childCount() > 0)
- iterateChildrenOf(c, lambda);
- else
- lambda(c);
- }
- }
-
- template<typename Func>
- void iterateChildrenOf(const Element &e, Func &&lambda) const
- {
- const qsizetype lastChildIndex = e.m_firstChildIndex + e.childCount() - 1;
- for (qsizetype i = e.m_firstChildIndex; i <= lastChildIndex; i++) {
- const Element &c = m_childElements[i];
- if (c.childCount() > 0)
- iterateChildrenOf(c, lambda);
- else
- lambda(c);
- }
- }
-
- template<typename Func>
- void iterateElements(Func &&lambda)
- {
- for (auto &e : m_elements) {
- if (e.childCount() > 0)
- iterateChildrenOf(e, lambda);
- else
- lambda(e);
- }
- }
-
- template<typename Func>
- void iterateElements(Func &&lambda) const
- {
- for (auto &e : m_elements) {
- if (e.childCount() > 0)
- iterateChildrenOf(e, lambda);
- else
- lambda(e);
- }
- }
-
- void splitElementAt(qsizetype index);
- Element &elementAt(qsizetype i) { return i < 0 ? m_childElements[-(i + 1)] : m_elements[i]; }
- const Element &elementAt(qsizetype i) const
- {
- return i < 0 ? m_childElements[-(i + 1)] : m_elements[i];
- }
-
- qsizetype indexOfChildAt(qsizetype i, qsizetype childNumber) const
- {
- return elementAt(i).indexOfChild(childNumber);
- }
-
- void addCurvatureData();
- bool contains(const QVector2D &v) const;
-
-private:
- void addElement(const QVector2D &control, const QVector2D &to, bool isLine = false);
- Element::CurvatureFlags coordinateOrderOfElement(const Element &element) const;
- static bool isControlPointOnLeft(const Element &element);
- static QVector2D closestPointOnLine(const QVector2D &start,
- const QVector2D &end,
- const QVector2D &p);
- static bool isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
- static bool isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
- static bool isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep);
-
- friend QDebug operator<<(QDebug, const QuadPath &);
-
- bool subPathToStart = true;
- Qt::FillRule m_fillRule = Qt::OddEvenFill;
- QVector2D currentPoint;
- QList<Element> m_elements;
- QList<Element> m_childElements;
-};
-
-QDebug operator<<(QDebug, const QuadPath::Element &);
-QDebug operator<<(QDebug, const QuadPath &);
-
class QQuickShapeCurveRenderer : public QQuickAbstractPathRenderer
{
public:
@@ -316,9 +88,9 @@ private:
FillGradientType gradientType = NoGradient;
GradientDesc gradient;
QPainterPath originalPath;
- QuadPath path;
- QuadPath fillPath;
- QuadPath strokePath;
+ QQuadPath path;
+ QQuadPath fillPath;
+ QQuadPath strokePath;
QColor fillColor;
Qt::FillRule fillRule = Qt::OddEvenFill;
QPen pen;
@@ -338,7 +110,7 @@ private:
QVector<QSGGeometryNode *> addTriangulatingStrokerNodes(const PathData &pathData, NodeList *debugNodes);
QVector<QSGGeometryNode *> addCurveStrokeNodes(const PathData &pathData, NodeList *debugNodes);
- void solveOverlaps(QuadPath &path);
+ void solveOverlaps(QQuadPath &path);
QSGNode *m_rootNode;
QVector<PathData> m_paths;
diff --git a/src/quickshapes/qquickshapecurverenderer_p_p.h b/src/quickshapes/qquickshapecurverenderer_p_p.h
index 40d0f3beb4..8829bd89a8 100644
--- a/src/quickshapes/qquickshapecurverenderer_p_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p_p.h
@@ -17,18 +17,12 @@
//
#include <QtQuickShapes/private/qquickshapesglobal_p.h>
-#include <QtGui/qvector2d.h>
-#include <QPainterPath>
#include <QLoggingCategory>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcShapeCurveRenderer);
-class QBezier;
-Q_QUICKSHAPES_PRIVATE_EXPORT void qt_toQuadratics(const QBezier &b, QPolygonF *out,
- qreal errorLimit = 0.01);
-
QT_END_NAMESPACE
#endif //QQUICKSHAPECURVERENDERER_P_P_H
diff --git a/src/quickshapes/qquickshapestrokenode.cpp b/src/quickshapes/qquickshapestrokenode.cpp
index 624ddd094c..2b07cd5bca 100644
--- a/src/quickshapes/qquickshapestrokenode.cpp
+++ b/src/quickshapes/qquickshapestrokenode.cpp
@@ -19,80 +19,54 @@ void QQuickShapeStrokeNode::QQuickShapeStrokeNode::updateMaterial()
setMaterial(m_material.data());
}
-// Find the parameters H, G for the depressed cubic
-// t^2+H*t+G=0
-// that results from the equation
-// Q'(s).(p-Q(s)) = 0
-// The last parameter is the static offset between s and t:
-// s = t - b/(3a)
-// use it to get back the parameter t
-QVector3D QQuickShapeStrokeNode::HGforPoint(QVector2D q_a, QVector2D q_b, QVector2D q_c, QVector2D p)
-{
- // this is a constant for the curve
- float a = -2. * QVector2D::dotProduct(q_a, q_a);
- // this is a constant for the curve
- float b = -3. * QVector2D::dotProduct(q_a, q_b);
- //this is linear in p so it can be put into the shader with vertex data
- float c = 2. * QVector2D::dotProduct(q_a, p) - QVector2D::dotProduct(q_b, q_b) - 2. * QVector2D::dotProduct(q_a, q_c);
- //this is linear in p so it can be put into the shader with vertex data
- float d = QVector2D::dotProduct(q_b,p) - QVector2D::dotProduct(q_b, q_c);
- // convert to depressed cubic.
- // both functions are linear in c and d and thus linear in p
- // Put in vertex data.
- float H = (3. * a * c - b * b) / (3. * a * a);
- float G = (2. * b * b * b - 9. * a * b * c + 27. * a * a * d) / (27. * a * a * a);
-
- return QVector3D(H, G, b/(3*a));
-}
-
// Take the start, control and end point of a curve and return the points A, B, C
// representing the curve as Q(s) = A*s*s + B*s + C
-std::array<QVector2D, 3> QQuickShapeStrokeNode::curveABC(QVector2D p0, QVector2D p1, QVector2D p2)
+std::array<QVector2D, 3> QQuickShapeStrokeNode::curveABC(const std::array<QVector2D, 3> &p)
{
- QVector2D a = p0 - 2*p1 + p2;
- QVector2D b = 2*p1 - 2*p0;
- QVector2D c = p0;
+ QVector2D a = p[0] - 2*p[1] + p[2];
+ QVector2D b = 2*p[1] - 2*p[0];
+ QVector2D c = p[0];
return {a, b, c};
}
-void QQuickShapeStrokeNode::appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2,
- const QVector2D &p0, const QVector2D &p1, const QVector2D &p2)
+// Curve from p[0] to p[2] with control point p[1]
+void QQuickShapeStrokeNode::appendTriangle(const std::array<QVector2D, 3> &v,
+ const std::array<QVector2D, 3> &p,
+ const std::array<QVector2D, 3> &n)
{
- auto abc = curveABC(p0, p1, p2);
+ auto abc = curveABC(p);
int currentVertex = m_uncookedVertexes.count();
- for (auto p : QList<QVector2D>({v0, v1, v2})) {
- auto hg = HGforPoint(abc[0], abc[1], abc[2], p);
-
- m_uncookedVertexes.append( { p.x(), p.y(),
- abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(),
- hg.x(), hg.y(),
- hg.z()} );
+ for (int i = 0; i < 3; ++i) {
+ m_uncookedVertexes.append( { v[i].x(), v[i].y(),
+ abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(),
+ n[i].x(), n[i].y() } );
}
m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2;
}
-void QQuickShapeStrokeNode::appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2,
- const QVector2D &p0, const QVector2D &p1)
+// Straight line from p0 to p1
+void QQuickShapeStrokeNode::appendTriangle(const std::array<QVector2D, 3> &v,
+ const std::array<QVector2D, 2> &p,
+ const std::array<QVector2D, 3> &n)
{
// We could reduce this to a linear equation by setting A to (0,0).
// However, then we cannot use the cubic solution and need an additional
// code path in the shader. The following formulation looks more complicated
// but allows to always use the cubic solution.
- auto A = p1 - p0;
+ auto A = p[1] - p[0];
auto B = QVector2D(0., 0.);
- auto C = p0;
+ auto C = p[0];
int currentVertex = m_uncookedVertexes.count();
- for (auto p : QList<QVector2D>({v0, v1, v2})) {
- auto hg = HGforPoint(A, B, C, p);
- m_uncookedVertexes.append( { p.x(), p.y(),
- A.x(), A.y(), B.x(), B.y(), C.x(), C.y(),
- hg.x(), hg.y(),
- hg.z()} );
+// for (auto v : QList<QPair<QVector2D, QVector2D>>({{v0, n0}, {v1, n1}, {v2, n2}})) {
+ for (int i = 0; i < 3; ++i) {
+ m_uncookedVertexes.append( { v[i].x(), v[i].y(),
+ A.x(), A.y(), B.x(), B.y(), C.x(), C.y(),
+ n[i].x(), n[i].y() } );
}
m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2;
}
@@ -129,11 +103,9 @@ const QSGGeometry::AttributeSet &QQuickShapeStrokeNode::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), //HG
- QSGGeometry::Attribute::createWithAttributeType(5, 1, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //offset
-
+ QSGGeometry::Attribute::createWithAttributeType(4, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //normalVector
};
- static QSGGeometry::AttributeSet attrs = { 6, sizeof(StrokeVertex), data };
+ static QSGGeometry::AttributeSet attrs = { 5, sizeof(StrokeVertex), data };
return attrs;
}
diff --git a/src/quickshapes/qquickshapestrokenode_p.cpp b/src/quickshapes/qquickshapestrokenode_p.cpp
index 5dae78a78d..d5eb901c66 100644
--- a/src/quickshapes/qquickshapestrokenode_p.cpp
+++ b/src/quickshapes/qquickshapestrokenode_p.cpp
@@ -62,7 +62,13 @@ bool QQuickShapeStrokeMaterialShader::updateUniformData(RenderState &state, QSGM
memcpy(buf->data() + offset, &w, 4);
changed = true;
}
- //offset += 16;
+ 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/quickshapes/qquickshapestrokenode_p.h b/src/quickshapes/qquickshapestrokenode_p.h
index e7ed4d17bb..8ec000173f 100644
--- a/src/quickshapes/qquickshapestrokenode_p.h
+++ b/src/quickshapes/qquickshapestrokenode_p.h
@@ -47,11 +47,12 @@ public:
return m_strokeWidth;
}
- void appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2, // triangle vertices
- const QVector2D &p0, const QVector2D &p1, const QVector2D &p2); // curve points
-
- void appendTriangle(const QVector2D &v0, const QVector2D &v1, const QVector2D &v2, // triangle vertices
- const QVector2D &p0, const QVector2D &p1); // line points
+ void appendTriangle(const std::array<QVector2D, 3> &v, // triangle vertices
+ const std::array<QVector2D, 3> &p, // curve points
+ const std::array<QVector2D, 3> &n); // vertex normals
+ void appendTriangle(const std::array<QVector2D, 3> &v, // triangle vertices
+ const std::array<QVector2D, 2> &p, // line points
+ const std::array<QVector2D, 3> &n); // vertex normals
void cookGeometry();
@@ -62,6 +63,16 @@ public:
return m_uncookedIndexes;
}
+ float debug() const
+ {
+ return m_debug;
+ }
+
+ void setDebug(float newDebug)
+ {
+ m_debug = newDebug;
+ }
+
private:
struct StrokeVertex
@@ -70,17 +81,16 @@ private:
float ax, ay;
float bx, by;
float cx, cy;
- float H, G; //depressed cubic parameters
- float offset; //mapping between depressed and happy cubic
+ float nx, ny; //normal vector: direction to move vertext to account for AA
};
void updateMaterial();
- static QVector3D HGforPoint(QVector2D A, QVector2D B, QVector2D C, QVector2D p);
- static std::array<QVector2D, 3> curveABC(QVector2D p0, QVector2D p1, QVector2D p2);
+ static std::array<QVector2D, 3> curveABC(const std::array<QVector2D, 3> &p);
QColor m_color;
float m_strokeWidth = 0.0f;
+ float m_debug = 0.0f;
protected:
QScopedPointer<QQuickShapeStrokeMaterial> m_material;
diff --git a/src/quickshapes/qt_quadratic_bezier.cpp b/src/quickshapes/qt_quadratic_bezier.cpp
deleted file mode 100644
index 6a41a67872..0000000000
--- a/src/quickshapes/qt_quadratic_bezier.cpp
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright (C) 2023 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
-
-// ---------------- Cubic -> Quadratic path stuff - temporarily here -----------
-
-#include <private/qbezier_p.h>
-#include <QtMath>
-
-QT_BEGIN_NAMESPACE
-
-static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp)
-{
- 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 QPointF qt_quadraticForCubic(const QBezier &b)
-{
- const QLineF st = b.startTangent();
- const QLineF et = b.endTangent();
- 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)
-{
- auto isValidRoot = [](qreal r) {
- return qIsFinite(r) && (r > 0) && (!qFuzzyIsNull(float(r))) && (r < 1)
- && (!qFuzzyIsNull(float(r - 1)));
- };
-
- // normalize so pt1.x,pt1.y,pt4.y == 0
- QTransform xf;
- const QLineF l(orig.pt1(), orig.pt4());
- xf.rotate(l.angle());
- xf.translate(-orig.pt1().x(), -orig.pt1().y());
- const QBezier n = orig.mapBy(xf);
- Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().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 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) - (4 * a * c);
- if (rad < 0)
- return 0;
- const qreal sqr = qSqrt(rad);
- const qreal root1 = (-b + sqr) / (2 * a);
- const qreal root2 = (-b - sqr) / (2 * a);
-
- int res = 0;
- if (isValidRoot(root1))
- tpoints[res++] = root1;
- if (root2 != root1 && isValidRoot(root2))
- tpoints[res++] = root2;
-
- if (res == 2 && tpoints[0] > tpoints[1])
- qSwap(tpoints[0], tpoints[1]);
-
- return res;
-}
-
-static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff)
-{
- 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, maxSplits - 1, maxDiff);
- qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff);
- }
-}
-
-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;
- }
- }
-
- const QRectF cpr = b.bounds();
- 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;
- // 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, out, maxSubSplits, maxDiff);
- t0 = t1;
- }
-}
-
-QT_END_NAMESPACE
diff --git a/src/quickshapes/shaders_ng/shapecurve.frag b/src/quickshapes/shaders_ng/shapecurve.frag
index da614a9de3..1b58d4c439 100644
--- a/src/quickshapes/shaders_ng/shapecurve.frag
+++ b/src/quickshapes/shaders_ng/shapecurve.frag
@@ -1,19 +1,14 @@
#version 440
layout(location = 0) in vec4 qt_TexCoord;
-layout(location = 1) in vec4 debugColor;
+layout(location = 1) in vec4 gradient;
#if defined(LINEARGRADIENT)
layout(location = 2) in float gradTabIndex;
-# define NEXT_LOCATION 3
#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT)
layout(location = 2) in vec2 coord;
-# define NEXT_LOCATION 3
-#else
-# define NEXT_LOCATION 2
#endif
-layout(location = NEXT_LOCATION) in vec4 gradient;
layout(location = 0) out vec4 fragColor;
@@ -21,7 +16,7 @@ layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float matrixScale;
float opacity;
- float reserved2;
+ float debug;
float reserved3;
#if defined(STROKE)
@@ -133,6 +128,13 @@ void main()
float df = length(vec2(dfx, dfy));
#endif
+ float isLine = 1.0 - abs(qt_TexCoord.z);
+ float isCurve = 1.0 - isLine;
+ float debugR = isCurve * min(1.0, 1.0 - qt_TexCoord.z);
+ float debugG = isLine;
+ float debugB = isCurve * min(1.0, 1.0 - qt_TexCoord.z * -1.0) + debugG;
+ vec3 debugColor = vec3(debugR, debugG, debugB);
+
#if defined(STROKE)
float distance = (f / df); // distance from centre of fragment to line
@@ -148,8 +150,8 @@ void main()
vec4 combined = fill * (1.0 - stroke.a) + stroke * stroke.a;
// finally mix in debug
- fragColor = mix(combined, vec4(debugColor.rgb, 1.0), debugColor.a) * ubuf.opacity;
+ fragColor = mix(combined, vec4(debugColor, 1.0), ubuf.debug) * ubuf.opacity;
#else
- fragColor = mix(baseColor() * clamp(0.5 + f / df, 0.0, 1.0), vec4(debugColor.rgb, 1.0), debugColor.a) * ubuf.opacity;
+ fragColor = mix(baseColor() * clamp(0.5 + f / df, 0.0, 1.0), vec4(debugColor, 1.0), ubuf.debug) * ubuf.opacity;
#endif
}
diff --git a/src/quickshapes/shaders_ng/shapecurve.vert b/src/quickshapes/shaders_ng/shapecurve.vert
index c20a22f613..c9dfc93a4f 100644
--- a/src/quickshapes/shaders_ng/shapecurve.vert
+++ b/src/quickshapes/shaders_ng/shapecurve.vert
@@ -2,29 +2,22 @@
layout(location = 0) in vec4 vertexCoord;
layout(location = 1) in vec4 vertexTexCoord;
-layout(location = 2) in vec4 vertexDebugColor;
-layout(location = 3) in vec4 vertexGradient;
+layout(location = 2) in vec4 vertexGradient;
layout(location = 0) out vec4 qt_TexCoord;
-layout(location = 1) out vec4 debugColor;
+layout(location = 1) out vec4 gradient;
#if defined(LINEARGRADIENT)
layout(location = 2) out float gradTabIndex;
-# define NEXT_LOCATION 3
#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT)
layout(location = 2) out vec2 coord;
-# define NEXT_LOCATION 3
-#else
-# define NEXT_LOCATION 2
#endif
-layout(location = NEXT_LOCATION) out vec4 gradient;
-
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float matrixScale;
float opacity;
- float reserved2;
+ float debug;
float reserved3;
#if defined(STROKE)
@@ -56,7 +49,6 @@ out gl_PerVertex { vec4 gl_Position; };
void main()
{
qt_TexCoord = vertexTexCoord;
- debugColor = vertexDebugColor;
gradient = vertexGradient / ubuf.matrixScale;
#if defined(LINEARGRADIENT)
diff --git a/src/quickshapes/shaders_ng/shapestroke.frag b/src/quickshapes/shaders_ng/shapestroke.frag
index b7f4a09f89..ac6ec6452f 100644
--- a/src/quickshapes/shaders_ng/shapestroke.frag
+++ b/src/quickshapes/shaders_ng/shapestroke.frag
@@ -20,7 +20,7 @@ layout(std140, binding = 0) uniform buf {
vec4 strokeColor;
float strokeWidth;
- float reserved4;
+ float debug;
float reserved5;
float reserved6;
} ubuf;
@@ -125,5 +125,7 @@ void main()
float centerline = step(ubuf.strokeWidth * 0.01, dmin);
fillCoverage = fillCoverage * centerline + min(1., ubuf.strokeWidth * ubuf.matrixScale) * (1. - centerline);
- fragColor = vec4(ubuf.strokeColor.rgb, 1.0) *ubuf.strokeColor.a * fillCoverage * ubuf.opacity;
+ 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/quickshapes/shaders_ng/shapestroke.vert b/src/quickshapes/shaders_ng/shapestroke.vert
index 90c67deb16..9b22eb4eeb 100644
--- a/src/quickshapes/shaders_ng/shapestroke.vert
+++ b/src/quickshapes/shaders_ng/shapestroke.vert
@@ -4,8 +4,7 @@ layout(location = 0) in vec4 vertexCoord;
layout(location = 1) in vec2 inA;
layout(location = 2) in vec2 inB;
layout(location = 3) in vec2 inC;
-layout(location = 4) in vec2 inHG;
-layout(location = 5) in float inoffset;
+layout(location = 4) in vec2 normalVector;
layout(location = 0) out vec4 P;
layout(location = 1) out vec2 A;
@@ -26,21 +25,52 @@ layout(std140, binding = 0) uniform buf {
vec4 strokeColor;
float strokeWidth;
- float reserved4;
+ float debug;
float reserved5;
float reserved6;
} ubuf;
out gl_PerVertex { vec4 gl_Position; };
+#define SQRT2 1.41421356237
+
+float qdot(vec2 a, vec2 b)
+{
+ return a.x * b.x + a.y * b.y;
+}
+
void main()
{
- P = vertexCoord;
+ P = vertexCoord + vec4(normalVector, 0.0, 0.0) * SQRT2/ubuf.matrixScale;
+
A = inA;
B = inB;
C = inC;
- HG = inHG;
- offset = inoffset;
- gl_Position = ubuf.qt_Matrix * vertexCoord;
+ // Find the parameters H, G for the depressed cubic
+ // t^2+H*t+G=0
+ // that results from the equation
+ // Q'(s).(p-Q(s)) = 0
+ // The last parameter is the static offset between s and t:
+ // s = t - b/(3a)
+ // use it to get back the parameter t
+
+ // this is a constant for the curve
+ float a = -2. * qdot(A, A);
+ // this is a constant for the curve
+ float b = -3. * qdot(A, B);
+ //this is linear in p so it can be put into the shader with vertex data
+ float c = 2. * qdot(A, P.xy) - qdot(B, B) - 2. * qdot(A, C);
+ //this is linear in p so it can be put into the shader with vertex data
+ float d = qdot(B, P.xy) - qdot(B, C);
+ // convert to depressed cubic.
+ // 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);
+
+
+
+ gl_Position = ubuf.qt_Matrix * P;
}
diff --git a/tests/manual/painterpathquickshape/ControlPoint.qml b/tests/manual/painterpathquickshape/ControlPoint.qml
index 77e7e4fbff..f180bdddf2 100644
--- a/tests/manual/painterpathquickshape/ControlPoint.qml
+++ b/tests/manual/painterpathquickshape/ControlPoint.qml
@@ -12,6 +12,7 @@ Rectangle {
opacity: 0.3
width: 20
height: 20
+ visible: !theMouseArea.pressed
property real cx: 400
property real cy: 800
diff --git a/tests/manual/painterpathquickshape/background.png b/tests/manual/painterpathquickshape/background.png
index 138103a4e0..9921459886 100644
--- a/tests/manual/painterpathquickshape/background.png
+++ b/tests/manual/painterpathquickshape/background.png
Binary files differ
diff --git a/tests/manual/painterpathquickshape/main.qml b/tests/manual/painterpathquickshape/main.qml
index c7c1f60579..36dae42639 100644
--- a/tests/manual/painterpathquickshape/main.qml
+++ b/tests/manual/painterpathquickshape/main.qml
@@ -146,7 +146,6 @@ Window {
anchors.fill: flickable
fillMode: Image.Tile
source: "qrc:/background.png"
- smooth: true
}
Rectangle {
id: solidBackground
@@ -196,7 +195,8 @@ Window {
id: loader
}
MouseArea {
- acceptedButtons: Qt.NoButton
+ id: theMouseArea
+ acceptedButtons: Qt.RightButton
anchors.fill: parent
onMouseXChanged: {
let p = Qt.point(Math.round(mouseX), Math.round(mouseY))