Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquadpath.cpp
Go to the documentation of this file.
1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquadpath_p.h"
5
6#include <private/qsgcurveprocessor_p.h>
7
8#include <QtGui/private/qbezier_p.h>
9#include <QtMath>
10#include <QtCore/QLoggingCategory>
11#include <QtCore/QVarLengthArray>
12
14
15static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp)
16{
17 // constexpr auto [t2s, tmts] = [] { // not allowed by C++ :(
18 constexpr auto precomputed = [] {
19 // Precompute bezier factors
20 constexpr int numSteps = 21;
21 static_assert(numSteps % 2 == 1, "numSteps must be odd");
22 struct R {
23 std::array<qreal, numSteps> t2s, tmts;
24 } r = {};
25 auto &[t2s, tmts] = r;
26 qreal t = 0.20;
27 const qreal step = (1 - (2 * t)) / (numSteps - 1);
28 for (int i = 0; i < numSteps; i++) {
29 t2s[i] = t * t;
30 tmts[i] = 2 * t * (1 - t);
31 t += step;
32 }
33 return r;
34 }();
35 constexpr auto t2s = precomputed.t2s;
36 constexpr auto tmts = precomputed.tmts;
37 constexpr auto numSteps = t2s.size();
38
39 const QPointF midPoint = b.midPoint();
40 auto distForIndex = [&](int i) -> qreal {
41 QPointF qp = (t2s[numSteps - 1 - i] * b.pt1()) + (tmts[i] * qcp) + (t2s[i] * b.pt4());
42 QPointF d = midPoint - qp;
43 return QPointF::dotProduct(d, d);
44 };
45
46 const int halfSteps = (numSteps - 1) / 2;
47 bool foundIt = false;
48 const qreal centerDist = distForIndex(halfSteps);
49 qreal minDist = centerDist;
50 // Search for the minimum in right half
51 for (int i = 0; i < halfSteps; i++) {
52 qreal tDist = distForIndex(halfSteps + 1 + i);
53 if (tDist < minDist) {
54 minDist = tDist;
55 } else {
56 foundIt = (i > 0);
57 break;
58 }
59 }
60 if (!foundIt) {
61 // Search in left half
62 minDist = centerDist;
63 for (int i = 0; i < halfSteps; i++) {
64 qreal tDist = distForIndex(halfSteps - 1 - i);
65 if (tDist < minDist) {
66 minDist = tDist;
67 } else {
68 foundIt = (i > 0);
69 break;
70 }
71 }
72 }
73 return foundIt ? minDist : centerDist;
74}
75
76static QPointF qt_quadraticForCubic(const QBezier &b)
77{
78 const QLineF st = b.startTangent();
79 const QLineF et = b.endTangent();
80 const QPointF midPoint = b.midPoint();
81 bool valid = true;
82 QPointF quadControlPoint;
83 if (st.intersects(et, &quadControlPoint) == QLineF::NoIntersection) {
84 valid = false;
85 } else {
86 // Check if intersection is on wrong side
87 const QPointF bl = b.pt4() - b.pt1();
88 const QPointF ml = midPoint - b.pt1();
89 const QPointF ql = quadControlPoint - b.pt1();
90 qreal cx1 = (ml.x() * bl.y()) - (ml.y() * bl.x());
91 qreal cx2 = (ql.x() * bl.y()) - (ql.y() * bl.x());
92 valid = (std::signbit(cx1) == std::signbit(cx2));
93 }
94 return valid ? quadControlPoint : midPoint;
95}
96
97static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
98{
99 auto isValidRoot = [](qreal r) {
100 return qIsFinite(r) && (r > 0) && (!qFuzzyIsNull(float(r))) && (r < 1)
101 && (!qFuzzyIsNull(float(r - 1)));
102 };
103
104 // normalize so pt1.x,pt1.y,pt4.y == 0
105 QTransform xf;
106 const QLineF l(orig.pt1(), orig.pt4());
107 xf.rotate(l.angle());
108 xf.translate(-orig.pt1().x(), -orig.pt1().y());
109 const QBezier n = orig.mapBy(xf);
110
111 const qreal x2 = n.pt2().x();
112 const qreal x3 = n.pt3().x();
113 const qreal x4 = n.pt4().x();
114 const qreal y2 = n.pt2().y();
115 const qreal y3 = n.pt3().y();
116
117 const qreal p = x3 * y2;
118 const qreal q = x4 * y2;
119 const qreal r = x2 * y3;
120 const qreal s = x4 * y3;
121
122 const qreal a = 18 * ((-3 * p) + (2 * q) + (3 * r) - s);
123 if (qFuzzyIsNull(float(a))) {
124 if (std::signbit(y2) != std::signbit(y3) && qFuzzyCompare(float(x4 - x3), float(x2))) {
125 tpoints[0] = 0.5; // approx
126 return 1;
127 } else if (!a) {
128 return 0;
129 }
130 }
131 const qreal b = 18 * (((3 * p) - q) - (3 * r));
132 const qreal c = 18 * (r - p);
133 const qreal rad = (b * b) - (4 * a * c);
134 if (rad < 0)
135 return 0;
136 const qreal sqr = qSqrt(rad);
137 const qreal root1 = (-b + sqr) / (2 * a);
138 const qreal root2 = (-b - sqr) / (2 * a);
139
140 int res = 0;
141 if (isValidRoot(root1))
142 tpoints[res++] = root1;
143 if (root2 != root1 && isValidRoot(root2))
144 tpoints[res++] = root2;
145
146 if (res == 2 && tpoints[0] > tpoints[1])
147 qSwap(tpoints[0], tpoints[1]);
148
149 return res;
150}
151
152static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff)
153{
154 QPointF qcp = qt_quadraticForCubic(b);
155 if (maxSplits <= 0 || qt_scoreQuadratic(b, qcp) < maxDiff) {
156 p->append(qcp);
157 p->append(b.pt4());
158 } else {
159 QBezier rhs = b;
160 QBezier lhs;
161 rhs.parameterSplitLeft(0.5, &lhs);
162 qt_addToQuadratics(lhs, p, maxSplits - 1, maxDiff);
163 qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff);
164 }
165}
166
167static void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit = 0.01)
168{
169 out->resize(0);
170 out->append(b.pt1());
171
172 {
173 // Shortcut if the cubic is really a quadratic
174 const qreal f = 3.0 / 2.0;
175 const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1());
176 const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4());
177 if (c1 == c2) {
178 out->append(c1);
179 out->append(b.pt4());
180 return;
181 }
182 }
183
184 const QRectF cpr = b.bounds();
185 const QPointF dim = cpr.bottomRight() - cpr.topLeft();
186 qreal maxDiff = QPointF::dotProduct(dim, dim) * errorLimit * errorLimit; // maxdistance^2
187
188 qreal infPoints[2];
189 int numInfPoints = qt_getInflectionPoints(b, infPoints);
190 const int maxSubSplits = numInfPoints > 0 ? 2 : 3;
191 qreal t0 = 0;
192 // number of main segments == #inflectionpoints + 1
193 for (int i = 0; i < numInfPoints + 1; i++) {
194 qreal t1 = (i < numInfPoints) ? infPoints[i] : 1;
195 QBezier segment = b.bezierOnInterval(t0, t1);
196 qt_addToQuadratics(segment, out, maxSubSplits, maxDiff);
197 t0 = t1;
198 }
199}
200
201QVector2D QQuadPath::Element::pointAtFraction(float t) const
202{
203 if (isLine()) {
204 return sp + t * (ep - sp);
205 } else {
206 const float r = 1 - t;
207 return (r * r * sp) + (2 * t * r * cp) + (t * t * ep);
208 }
209}
210
211QQuadPath::Element QQuadPath::Element::segmentFromTo(float t0, float t1) const
212{
213 if (t0 <= 0 && t1 >= 1)
214 return *this;
215
216 Element part;
217 part.sp = pointAtFraction(t0);
218 part.ep = pointAtFraction(t1);
219
220 if (isLine()) {
221 part.cp = 0.5f * (part.sp + part.ep);
222 part.m_isLine = true;
223 } else {
224 // Split curve right at t0, yields { t0, rcp, endPoint } quad segment
225 const QVector2D rcp = (1 - t0) * controlPoint() + t0 * endPoint();
226 // Split that left at t1, yields { t0, lcp, t1 } quad segment
227 float segmentT = (t1 - t0) / (1 - t0);
228 part.cp = (1 - segmentT) * part.sp + segmentT * rcp;
229 }
230 return part;
231}
232
233QQuadPath::Element QQuadPath::Element::reversed() const {
234 Element swappedElement;
235 swappedElement.ep = sp;
236 swappedElement.cp = cp;
237 swappedElement.sp = ep;
238 swappedElement.m_isLine = m_isLine;
239 return swappedElement;
240}
241
242float QQuadPath::Element::extent() const
243{
244 // TBD: cache this value if we start using it a lot
245 QVector2D min(qMin(sp.x(), ep.x()), qMin(sp.y(), ep.y()));
246 QVector2D max(qMax(sp.x(), ep.x()), qMax(sp.y(), ep.y()));
247 if (!isLine()) {
248 min = QVector2D(qMin(min.x(), cp.x()), qMin(min.y(), cp.y()));
249 max = QVector2D(qMax(max.x(), cp.x()), qMax(max.y(), cp.y()));
250 }
251 return (max - min).length();
252}
253
254// Returns the number of intersections between element and a horizontal line at y.
255// The t values of max 2 intersection(s) are stored in the fractions array
256int QQuadPath::Element::intersectionsAtY(float y, float *fractions, bool swapXY) const
257{
258 Q_ASSERT(!isLine());
259
260 auto getY = [=](QVector2D p) -> float { return swapXY ? -p.x() : p.y(); };
261
262 const float y0 = getY(startPoint()) - y;
263 const float y1 = getY(controlPoint()) - y;
264 const float y2 = getY(endPoint()) - y;
265
266 int numRoots = 0;
267 const float a = y0 - (2 * y1) + y2;
268 if (a) {
269 const float b = (y1 * y1) - (y0 * y2);
270 if (b >= 0) {
271 const float sqr = qSqrt(b);
272 const float root1 = -(-y0 + y1 + sqr) / a;
273 if (qIsFinite(root1) && root1 >= 0 && root1 <= 1)
274 fractions[numRoots++] = root1;
275 const float root2 = (y0 - y1 + sqr) / a;
276 if (qIsFinite(root2) && root2 != root1 && root2 >= 0 && root2 <= 1)
277 fractions[numRoots++] = root2;
278 }
279 } else if (y1 != y2) {
280 const float root1 = (y2 - (2 * y1)) / (2 * (y2 - y1));
281 if (qIsFinite(root1) && root1 >= 0 && root1 <= 1)
282 fractions[numRoots++] = root1;
283 }
284
285 return numRoots;
286}
287
288static float crossProduct(const QVector2D &sp, const QVector2D &p, const QVector2D &ep)
289{
290 QVector2D v1 = ep - sp;
291 QVector2D v2 = p - sp;
292 return (v2.x() * v1.y()) - (v2.y() * v1.x());
293}
294
295bool QQuadPath::isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
296{
297 // Use cross product to compare directions of base vector and vector from start to p
298 return crossProduct(sp, p, ep) >= 0.0f;
299}
300
301bool QQuadPath::isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
302{
303 return qFuzzyIsNull(crossProduct(sp, p, ep));
304}
305
306// Assumes sp != ep
307bool QQuadPath::isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
308{
309 // epsilon is max length of p-to-baseline relative to length of baseline. So 0.01 means that
310 // the distance from p to the baseline must be less than 1% of the length of the baseline.
311 constexpr float epsilon = 0.01f;
312 QVector2D bv = ep - sp;
313 float bl2 = QVector2D::dotProduct(bv, bv);
314 float t = QVector2D::dotProduct(p - sp, bv) / bl2;
315 QVector2D pv = p - (sp + t * bv);
316 return (QVector2D::dotProduct(pv, pv) / bl2) < (epsilon * epsilon);
317}
318
319QVector2D QQuadPath::closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep)
320{
321 QVector2D line = ep - sp;
322 float t = QVector2D::dotProduct(p - sp, line) / QVector2D::dotProduct(line, line);
323 return sp + qBound(0.0f, t, 1.0f) * line;
324}
325
326// NOTE: it is assumed that subpaths are closed
327bool QQuadPath::contains(const QVector2D &point) const
328{
329 return contains(point, 0, elementCount() - 1);
330}
331
332bool QQuadPath::contains(const QVector2D &point, int fromIndex, int toIndex) const
333{
334 // if (!controlPointRect().contains(pt) : good opt when we add cpr caching
335 // return false;
336
337 int winding_number = 0;
338 for (int ei = fromIndex; ei <= toIndex; ei++) {
339 const Element &e = m_elements.at(ei);
340 int dir = 1;
341 float y1 = e.startPoint().y();
342 float y2 = e.endPoint().y();
343 if (y2 < y1) {
344 qSwap(y1, y2);
345 dir = -1;
346 }
347 if (e.m_isLine) {
348 if (point.y() < y1 || point.y() >= y2 || y1 == y2)
349 continue;
350 const float t = (point.y() - e.startPoint().y()) / (e.endPoint().y() - e.startPoint().y());
351 const float x = e.startPoint().x() + t * (e.endPoint().x() - e.startPoint().x());
352 if (x <= point.x())
353 winding_number += dir;
354 } else {
355 y1 = qMin(y1, e.controlPoint().y());
356 y2 = qMax(y2, e.controlPoint().y());
357 if (point.y() < y1 || point.y() >= y2)
358 continue;
359 float ts[2];
360 const int numRoots = e.intersectionsAtY(point.y(), ts);
361 // Count if there is exactly one intersection to the left
362 bool oneHit = false;
363 float tForHit = -1;
364 for (int i = 0; i < numRoots; i++) {
365 if (e.pointAtFraction(ts[i]).x() <= point.x()) {
366 oneHit = !oneHit;
367 tForHit = ts[i];
368 }
369 }
370 if (oneHit) {
371 float ty = e.tangentAtFraction(tForHit).y();
372 if (qFuzzyIsNull(ty)) {
373 // Horizontal tangent is ambiguous, check second derivative
374 const float b = e.endPoint().y() - 2 * e.controlPoint().y() + e.startPoint().y();
375 ty = (tForHit < 0.5f) ? b : -b;
376 }
377 if (ty != 0.0f) {
378 dir = ty < 0 ? -1 : 1;
379 winding_number += dir;
380 }
381 }
382 }
383 };
384
385 return (fillRule() == Qt::WindingFill ? (winding_number != 0) : ((winding_number % 2) != 0));
386}
387
388// similar as contains. But we treat the element with the index elementIdx in a special way
389// that should be numerically more stable. The result is a contains for a point on the left
390// and for the right side of the element.
391QQuadPath::Element::FillSide QQuadPath::fillSideOf(int elementIdx, float elementT) const
392{
393 constexpr float toleranceT = 1e-3f;
394 const QVector2D point = m_elements.at(elementIdx).pointAtFraction(elementT);
395 const QVector2D tangent = m_elements.at(elementIdx).tangentAtFraction(elementT);
396
397 const bool swapXY = qAbs(tangent.x()) > qAbs(tangent.y());
398 auto getX = [=](QVector2D p) -> float { return swapXY ? p.y() : p.x(); };
399 auto getY = [=](QVector2D p) -> float { return swapXY ? -p.x() : p.y(); };
400
401 int winding_number = 0;
402 for (int i = 0; i < elementCount(); i++) {
403 const Element &e = m_elements.at(i);
404 int dir = 1;
405 float y1 = getY(e.startPoint());
406 float y2 = getY(e.endPoint());
407 if (y2 < y1) {
408 qSwap(y1, y2);
409 dir = -1;
410 }
411 if (e.m_isLine) {
412 if (getY(point) < y1 || getY(point) >= y2 || y1 == y2)
413 continue;
414 const float t = (getY(point) - getY(e.startPoint())) / (getY(e.endPoint()) - getY(e.startPoint()));
415 const float x = getX(e.startPoint()) + t * (getX(e.endPoint()) - getX(e.startPoint()));
416 if (x <= getX(point) && (i != elementIdx || qAbs(t - elementT) > toleranceT))
417 winding_number += dir;
418 } else {
419 y1 = qMin(y1, getY(e.controlPoint()));
420 y2 = qMax(y2, getY(e.controlPoint()));
421 if (getY(point) < y1 || getY(point) >= y2)
422 continue;
423 float ts[2];
424 const int numRoots = e.intersectionsAtY(getY(point), ts, swapXY);
425 // Count if there is exactly one intersection to the left
426 bool oneHit = false;
427 float tForHit = -1;
428 for (int j = 0; j < numRoots; j++) {
429 const float x = getX(e.pointAtFraction(ts[j]));
430 if (x <= getX(point) && (i != elementIdx || qAbs(ts[j] - elementT) > toleranceT)) {
431 oneHit = !oneHit;
432 tForHit = ts[j];
433 }
434 }
435 if (oneHit) {
436 dir = getY(e.tangentAtFraction(tForHit)) < 0 ? -1 : 1;
437 winding_number += dir;
438 }
439 }
440 };
441
442 int left_winding_number = winding_number;
443 int right_winding_number = winding_number;
444
445 int dir = getY(tangent) < 0 ? -1 : 1;
446
447 if (dir > 0)
448 left_winding_number += dir;
449 else
450 right_winding_number += dir;
451
452 bool leftInside = (fillRule() == Qt::WindingFill ? (left_winding_number != 0) : ((left_winding_number % 2) != 0));
453 bool rightInside = (fillRule() == Qt::WindingFill ? (right_winding_number != 0) : ((right_winding_number % 2) != 0));
454
455 if (leftInside && rightInside)
456 return QQuadPath::Element::FillSideBoth;
457 else if (leftInside)
458 return QQuadPath::Element::FillSideLeft;
459 else if (rightInside)
460 return QQuadPath::Element::FillSideRight;
461 else
462 return QQuadPath::Element::FillSideUndetermined; //should not happen except for numerical error.
463}
464
465void QQuadPath::addElement(const QVector2D &control, const QVector2D &endPoint, bool isLine)
466{
467 if (qFuzzyCompare(m_currentPoint, endPoint))
468 return; // 0 length element, skip
469
470 isLine = isLine || isPointNearLine(control, m_currentPoint, endPoint); // Turn flat quad into line
471
472 if (!m_subPathToStart) {
473 Q_ASSERT(!m_elements.isEmpty());
474 m_elements.last().m_isSubpathEnd = false;
475 }
476 m_elements.resize(m_elements.size() + 1);
477 Element &elem = m_elements.last();
478 elem.sp = m_currentPoint;
479 elem.cp = isLine ? (0.5f * (m_currentPoint + endPoint)) : control;
480 elem.ep = endPoint;
481 elem.m_isLine = isLine;
482 elem.m_isSubpathStart = m_subPathToStart;
483 m_subPathToStart = false;
484 elem.m_isSubpathEnd = true;
485 m_currentPoint = endPoint;
486}
487
488void QQuadPath::addElement(const Element &e)
489{
490 m_subPathToStart = false;
491 m_currentPoint = e.endPoint();
492 m_elements.append(e);
493}
494
495#if !defined(QQUADPATH_CONVEX_CHECK_ERROR_MARGIN)
496# define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN (1.0f / 32.0f)
497#endif
498
499QQuadPath::Element::FillSide QQuadPath::coordinateOrderOfElement(const QQuadPath::Element &element) const
500{
501 QVector2D baseLine = element.endPoint() - element.startPoint();
502 QVector2D midPoint = element.midPoint();
503 // At the midpoint, the tangent of a quad is parallel to the baseline
504 QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
505 float delta = qMin(element.extent() / 100, QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN);
506 QVector2D offset = (normal * delta);
507 bool pathContainsPointToRight = contains(midPoint + offset);
508 bool pathContainsPointToLeft = contains(midPoint - offset);
509 Element::FillSide res = Element::FillSideUndetermined;
510 if (pathContainsPointToRight)
511 res = (pathContainsPointToLeft ? Element::FillSideBoth : Element::FillSideRight);
512 else if (pathContainsPointToLeft)
513 res = Element::FillSideLeft;
514 return res;
515}
516
517QQuadPath QQuadPath::fromPainterPath(const QPainterPath &path, PathHints hints)
518{
519 QQuadPath res;
520 res.reserve(path.elementCount());
521 res.setFillRule(path.fillRule());
522
523 const bool isQuadratic = hints & PathQuadratic;
524
525 QPolygonF quads;
526 QPointF sp;
527 for (int i = 0; i < path.elementCount(); ++i) {
528 QPainterPath::Element element = path.elementAt(i);
529
530 QPointF ep(element);
531 switch (element.type) {
532 case QPainterPath::MoveToElement:
533 res.moveTo(QVector2D(ep));
534 break;
535 case QPainterPath::LineToElement:
536 res.lineTo(QVector2D(ep));
537 break;
538 case QPainterPath::CurveToElement: {
539 QPointF cp1 = ep;
540 QPointF cp2(path.elementAt(++i));
541 ep = path.elementAt(++i);
542 if (isQuadratic) {
543 const qreal f = 3.0 / 2.0;
544 const QPointF cp = sp + f * (cp1 - sp);
545 res.quadTo(QVector2D(cp), QVector2D(ep));
546 } else {
547 QBezier b = QBezier::fromPoints(sp, cp1, cp2, ep);
548 qt_toQuadratics(b, &quads);
549 for (int i = 1; i < quads.size(); i += 2) {
550 QVector2D cp(quads[i]);
551 QVector2D ep(quads[i + 1]);
552 res.quadTo(cp, ep);
553 }
554 }
555 break;
556 }
557 default:
558 Q_UNREACHABLE();
559 break;
560 }
561 sp = ep;
562 }
563
564 res.setPathHints(hints | PathQuadratic);
565 return res;
566}
567
568static bool hasOverlappingLines(const QQuadPath &path)
569{
570 QVarLengthArray<int, 16> lines;
571 for (int i = 0; i < path.elementCount(); i++) {
572 const QQuadPath::Element &e = path.elementAt(i);
573 if (e.isLine()) {
574 const QVector2D tangent = e.tangentAtFraction(0);
575 const bool isHorizontal = qAbs(tangent.x()) > qAbs(tangent.y());
576 float e1 = isHorizontal ? e.startPoint().x() : e.startPoint().y();
577 float e2 = isHorizontal ? e.endPoint().x() : e.endPoint().y();
578 if (e1 > e2)
579 qSwap(e1, e2);
580 for (int j = 0; j < lines.size(); j++) {
581 const QQuadPath::Element &line = path.elementAt(j);
582 if (QQuadPath::isPointOnLine(e.controlPoint(), line.startPoint(), line.endPoint())) {
583 // Check overlap
584 float l1 = isHorizontal ? line.startPoint().x() : line.startPoint().y();
585 float l2 = isHorizontal ? line.endPoint().x() : line.endPoint().y();
586 if (l1 > l2)
587 qSwap(l1, l2);
588 if (qMax(l1, e1) <= qMin(l2, e2))
589 return true;
590 }
591 }
592 lines.append(i);
593 }
594 }
595 return false;
596}
597
598void QQuadPath::addCurvatureData()
599{
600 // We use the convention that the inside of a curve is on the *right* side of the
601 // direction of the baseline.Thus, as long as this is true: if the control point is
602 // on the left side of the baseline, the curve is convex and otherwise it is
603 // concave. The paths we get can be arbitrary order, but each subpath will have a
604 // consistent order. Therefore, for the first curve element in a subpath, we can
605 // determine if the direction already follows the convention or not, and then we
606 // can easily detect curvature of all subsequent elements in the subpath.
607
608 auto isSingleSided = [](Element::FillSide fillSide) {
609 return fillSide == Element::FillSideLeft || fillSide == Element::FillSideRight;
610 };
611
612 auto flagFromFillSide = [](Element::FillSide fillSide) {
613 if (fillSide == Element::FillSideRight || fillSide == Element::FillSideBoth)
614 return Element::FillOnRight;
615 else
616 return Element::CurvatureUndetermined;
617 };
618
619 static bool checkAnomalyEnv = qEnvironmentVariableIntValue("QT_QUICKSHAPES_CHECK_ALL_CURVATURE") != 0;
620 constexpr int overlapTestLimit = 64;
621 // Line overlap test becomes expensive for large counts; leave to manual override with CHECK_ALL
622 const bool checkAnomaly =
623 checkAnomalyEnv || (lineCount() <= overlapTestLimit && hasOverlappingLines(*this));
624 const bool pathHasFillOnRight = testHint(PathFillOnRight);
625
626 Element::CurvatureFlags flags = Element::CurvatureUndetermined;
627 for (int i = 0; i < m_elements.size(); i++) {
628 QQuadPath::Element &element = m_elements[i];
629 Q_ASSERT(element.childCount() == 0);
630 if (element.isSubpathStart()) {
631 if (pathHasFillOnRight && !checkAnomaly) {
632 flags = Element::FillOnRight;
633 } else {
634 Element::FillSide fillSide = Element::FillSideUndetermined;
635 for (int j = i; !isSingleSided(fillSide) && j < m_elements.size(); j++) {
636 const QQuadPath::Element &subElem = m_elements.at(j);
637 if (j > i && subElem.isSubpathStart())
638 break;
639 fillSide = coordinateOrderOfElement(subElem);
640 }
641 flags = flagFromFillSide(fillSide);
642 }
643 } else if (checkAnomaly) {
644 Element::FillSide fillSide = coordinateOrderOfElement(element);
645 if (isSingleSided(fillSide)) {
646 Element::CurvatureFlags newFlags = flagFromFillSide(fillSide);
647 if (flags != newFlags) {
648 qCDebug(lcSGCurveProcessor)
649 << "Curvature anomaly detected:" << element
650 << "Subpath fill on right:" << (flags & Element::FillOnRight)
651 << "Element fill on right:" << (newFlags & Element::FillOnRight);
652 flags = newFlags;
653 }
654 }
655 }
656
657 if (element.isLine()) {
658 element.m_curvatureFlags = flags;
659 } else {
660 bool controlPointOnLeft = element.isControlPointOnLeft();
661 bool isFillOnRight = flags & Element::FillOnRight;
662 bool isConvex = controlPointOnLeft == isFillOnRight;
663
664 if (isConvex)
665 element.m_curvatureFlags = Element::CurvatureFlags(flags | Element::Convex);
666 else
667 element.m_curvatureFlags = flags;
668 }
669 }
670}
671
672QRectF QQuadPath::controlPointRect() const
673{
674 QRectF res;
675 if (elementCount()) {
676 QVector2D min, max;
677 min = max = m_elements.constFirst().sp;
678 // No need to recurse, as split curve's controlpoints are within the parent curve's
679 for (const QQuadPath::Element &e : std::as_const(m_elements)) {
680 min.setX(std::min({ min.x(), e.sp.x(), e.cp.x(), e.ep.x() }));
681 min.setY(std::min({ min.y(), e.sp.y(), e.cp.y(), e.ep.y() }));
682 max.setX(std::max({ max.x(), e.sp.x(), e.cp.x(), e.ep.x() }));
683 max.setY(std::max({ max.y(), e.sp.y(), e.cp.y(), e.ep.y() }));
684 }
685 res = QRectF(min.toPointF(), max.toPointF());
686 }
687 return res;
688}
689
690// Count leaf elements
691int QQuadPath::elementCountRecursive() const
692{
693 int count = 0;
694 iterateElements([&](const QQuadPath::Element &, int) { count++; });
695 return count;
696}
697
698QPainterPath QQuadPath::toPainterPath() const
699{
700 // Currently only converts the main, unsplit path; no need for the split path identified
701 QPainterPath res;
702 res.reserve(elementCount());
703 res.setFillRule(fillRule());
704 for (const Element &element : m_elements) {
705 if (element.m_isSubpathStart)
706 res.moveTo(element.startPoint().toPointF());
707 if (element.m_isLine)
708 res.lineTo(element.endPoint().toPointF());
709 else
710 res.quadTo(element.controlPoint().toPointF(), element.endPoint().toPointF());
711 };
712 return res;
713}
714
715QString QQuadPath::asSvgString() const
716{
717 QString res;
718 QTextStream str(&res);
719 for (const Element &element : m_elements) {
720 if (element.isSubpathStart())
721 str << "M " << element.startPoint().x() << " " << element.startPoint().y() << " ";
722 if (element.isLine())
723 str << "L " << element.endPoint().x() << " " << element.endPoint().y() << " ";
724 else
725 str << "Q " << element.controlPoint().x() << " " << element.controlPoint().y() << " "
726 << element.endPoint().x() << " " << element.endPoint().y() << " ";
727 }
728 return res;
729}
730
731// Returns a new path since doing it inline would probably be less efficient
732// (technically changing it from O(n) to O(n^2))
733// Note that this function should be called before splitting any elements,
734// so we can assume that the structure is a list and not a tree
735QQuadPath QQuadPath::subPathsClosed(bool *didClose) const
736{
737 Q_ASSERT(m_childElements.isEmpty());
738 bool closed = false;
739 QQuadPath res = *this;
740 res.m_subPathToStart = false;
741 res.m_elements = {};
742 res.m_elements.reserve(elementCount());
743 int subStart = -1;
744 int prevElement = -1;
745 for (int i = 0; i < elementCount(); i++) {
746 const auto &element = m_elements.at(i);
747 if (element.m_isSubpathStart) {
748 if (subStart >= 0 && m_elements[i - 1].ep != m_elements[subStart].sp) {
749 res.m_currentPoint = m_elements[i - 1].ep;
750 res.lineTo(m_elements[subStart].sp);
751 closed = true;
752 auto &endElement = res.m_elements.last();
753 endElement.m_isSubpathEnd = true;
754 // lineTo() can bail out if the points are too close.
755 // In that case, just change the end point to be equal to the start
756 // (No need to test because the assignment is a no-op otherwise).
757 endElement.ep = m_elements[subStart].sp;
758 } else if (prevElement >= 0) {
759 res.m_elements[prevElement].m_isSubpathEnd = true;
760 }
761 subStart = i;
762 }
763 res.m_elements.append(element);
764 prevElement = res.m_elements.size() - 1;
765 }
766
767 if (subStart >= 0 && m_elements.last().ep != m_elements[subStart].sp) {
768 res.m_currentPoint = m_elements.last().ep;
769 res.lineTo(m_elements[subStart].sp);
770 closed = true;
771 }
772 if (!res.m_elements.isEmpty()) {
773 auto &endElement = res.m_elements.last();
774 endElement.m_isSubpathEnd = true;
775 endElement.ep = m_elements[subStart].sp;
776 }
777
778 if (didClose)
779 *didClose = closed;
780 return res;
781}
782
783QQuadPath QQuadPath::flattened() const
784{
785 QQuadPath res;
786 res.reserve(elementCountRecursive());
787 iterateElements([&](const QQuadPath::Element &elem, int) { res.m_elements.append(elem); });
788 res.setPathHints(pathHints());
789 res.setFillRule(fillRule());
790 return res;
791}
792
794{
795public:
796 ElementCutter(const QQuadPath::Element &element)
798 {
799 m_currentPoint = m_element.startPoint();
800 if (m_element.isLine())
801 m_lineLength = (m_element.endPoint() - m_element.startPoint()).length();
802 else
803 fillLUT();
804 }
805
806 bool consume(float length)
807 {
808 m_lastT = m_currentT;
809 m_lastPoint = m_currentPoint;
810 float nextCut = m_consumed + length;
811 float cutT = m_element.isLine() ? nextCut / m_lineLength : tForLength(nextCut);
812 if (cutT < 1) {
813 m_currentT = cutT;
814 m_currentPoint = m_element.pointAtFraction(m_currentT);
815 m_consumed = nextCut;
816 return true;
817 } else {
818 m_currentT = 1;
819 m_currentPoint = m_element.endPoint();
820 return false;
821 }
822 }
823
825 {
826 return m_currentPoint;
827 }
828
830 {
831 Q_ASSERT(!m_element.isLine());
832 // Split curve right at lastT, yields { lastPoint, rcp, endPoint } quad segment
833 QVector2D rcp = (1 - m_lastT) * m_element.controlPoint() + m_lastT * m_element.endPoint();
834 // Split that left at currentT, yields { lastPoint, lcp, currentPoint } quad segment
835 float segmentT = (m_currentT - m_lastT) / (1 - m_lastT);
836 QVector2D lcp = (1 - segmentT) * m_lastPoint + segmentT * rcp;
837 return lcp;
838 }
839
841 {
842 float elemLength = m_element.isLine() ? m_lineLength : m_lut.last();
843 return elemLength - m_consumed;
844 }
845
846private:
847 void fillLUT()
848 {
849 Q_ASSERT(!m_element.isLine());
850 QVector2D ap = m_element.startPoint() - 2 * m_element.controlPoint() + m_element.endPoint();
851 QVector2D bp = 2 * m_element.controlPoint() - 2 * m_element.startPoint();
852 float A = 4 * QVector2D::dotProduct(ap, ap);
853 float B = 4 * QVector2D::dotProduct(ap, bp);
854 float C = QVector2D::dotProduct(bp, bp);
855 float b = B / (2 * A);
856 float c = C / A;
857 float k = c - (b * b);
858 float l2 = b * std::sqrt(b * b + k);
859 float lnom = b + std::sqrt(b * b + k);
860 float l0 = 0.5f * std::sqrt(A);
861
862 m_lut.resize(LUTSize, 0);
863 for (int i = 1; i < LUTSize; i++) {
864 float t = float(i) / (LUTSize - 1);
865 float u = t + b;
866 float w = std::sqrt(u * u + k);
867 float l1 = u * w;
868 float lden = u + w;
869 float l3 = k * std::log(std::fabs(lden / lnom));
870 float res = l0 * (l1 - l2 + l3);
871 m_lut[i] = res;
872 }
873 }
874
875 float tForLength(float length)
876 {
877 Q_ASSERT(!m_element.isLine());
878 Q_ASSERT(!m_lut.isEmpty());
879
880 float res = 2; // I.e. invalid, outside [0, 1] range
881 auto it = std::upper_bound(m_lut.cbegin(), m_lut.cend(), length);
882 if (it != m_lut.cend()) {
883 float nextLength = *it--;
884 float prevLength = *it;
885 int prevIndex = std::distance(m_lut.cbegin(), it);
886 float fraction = (length - prevLength) / (nextLength - prevLength);
887 res = (prevIndex + fraction) / (LUTSize - 1);
888 }
889 return res;
890 }
891
893 float m_lastT = 0;
894 float m_currentT = 0;
895 QVector2D m_lastPoint;
896 QVector2D m_currentPoint;
897 float m_consumed = 0;
898 // For line elements:
899 float m_lineLength;
900 // For quadratic curve elements:
901 static constexpr int LUTSize = 21;
902 QVarLengthArray<float, LUTSize> m_lut;
903};
904
905QQuadPath QQuadPath::dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset) const
906{
907 QVarLengthArray<float, 16> pattern;
908 float patternLength = 0;
909 for (int i = 0; i < 2 * (dashPattern.length() / 2); i++) {
910 float dashLength = qMax(lineWidth * dashPattern[i], qreal(0));
911 pattern.append(dashLength);
912 patternLength += dashLength;
913 }
914 if (patternLength == 0)
915 return {};
916
917 int startIndex = 0;
918 float startOffset = std::fmod(lineWidth * dashOffset, patternLength);
919 if (startOffset < 0)
920 startOffset += patternLength;
921 for (float dashLength : pattern) {
922 if (dashLength > startOffset)
923 break;
924 startIndex = (startIndex + 1) % pattern.size(); // The % guards against accuracy issues
925 startOffset -= dashLength;
926 }
927
928 int dashIndex = startIndex;
929 float offset = startOffset;
930 QQuadPath res;
931 for (int i = 0; i < elementCount(); i++) {
932 const Element &element = elementAt(i);
933 if (element.isSubpathStart()) {
934 res.moveTo(element.startPoint());
935 dashIndex = startIndex;
936 offset = startOffset;
937 }
938 ElementCutter cutter(element);
939 while (true) {
940 bool gotAll = cutter.consume(pattern.at(dashIndex) - offset);
941 QVector2D nextPoint = cutter.currentCutPoint();
942 if (dashIndex & 1)
943 res.moveTo(nextPoint); // gap
944 else if (element.isLine())
945 res.lineTo(nextPoint); // dash in line
946 else
947 res.quadTo(cutter.currentControlPoint(), nextPoint); // dash in curve
948 if (gotAll) {
949 offset = 0;
950 dashIndex = (dashIndex + 1) % pattern.size();
951 } else {
952 offset += cutter.lastLength();
953 break;
954 }
955 }
956 }
957 res.setFillRule(fillRule());
958 res.setPathHints(pathHints());
959 return res;
960}
961
962void QQuadPath::splitElementAt(int index)
963{
964 const int newChildIndex = m_childElements.size();
965 m_childElements.resize(newChildIndex + 2);
966 Element &parent = elementAt(index);
967 parent.m_numChildren = 2;
968 parent.m_firstChildIndex = newChildIndex;
969
970 Element &quad1 = m_childElements[newChildIndex];
971 const QVector2D mp = parent.midPoint();
972 quad1.sp = parent.sp;
973 quad1.cp = 0.5f * (parent.sp + parent.cp);
974 quad1.ep = mp;
975 quad1.m_isSubpathStart = parent.m_isSubpathStart;
976 quad1.m_isSubpathEnd = false;
977 quad1.m_curvatureFlags = parent.m_curvatureFlags;
978 quad1.m_isLine = parent.m_isLine; //### || isPointNearLine(quad1.cp, quad1.sp, quad1.ep);
979
980 Element &quad2 = m_childElements[newChildIndex + 1];
981 quad2.sp = mp;
982 quad2.cp = 0.5f * (parent.ep + parent.cp);
983 quad2.ep = parent.ep;
984 quad2.m_isSubpathStart = false;
985 quad2.m_isSubpathEnd = parent.m_isSubpathEnd;
986 quad2.m_curvatureFlags = parent.m_curvatureFlags;
987 quad2.m_isLine = parent.m_isLine; //### || isPointNearLine(quad2.cp, quad2.sp, quad2.ep);
988
989#ifndef QT_NO_DEBUG
990 if (qFuzzyCompare(quad1.sp, quad1.ep) || qFuzzyCompare(quad2.sp, quad2.ep))
991 qCDebug(lcSGCurveProcessor) << "Splitting has resulted in ~null quad";
992#endif
993}
994
995static void printElement(QDebug stream, const QQuadPath::Element &element)
996{
997 auto printPoint = [&](QVector2D p) { stream << "(" << p.x() << ", " << p.y() << ") "; };
998 stream << "{ ";
999 printPoint(element.startPoint());
1000 printPoint(element.controlPoint());
1001 printPoint(element.endPoint());
1002 stream << "} " << (element.isLine() ? "L " : "C ") << (element.isConvex() ? "X " : "O ")
1003 << (element.isFillOnRight() ? "R " : "L ")
1004 << (element.isSubpathStart() ? "S" : element.isSubpathEnd() ? "E" : "");
1005}
1006
1007#ifndef QT_NO_DEBUG_STREAM
1008QDebug operator<<(QDebug stream, const QQuadPath::Element &element)
1009{
1010 QDebugStateSaver saver(stream);
1011 stream.nospace();
1012 stream << "QuadPath::Element( ";
1013 printElement(stream, element);
1014 stream << " )";
1015 return stream;
1016}
1017
1018QDebug operator<<(QDebug stream, const QQuadPath &path)
1019{
1020 QDebugStateSaver saver(stream);
1021 stream.nospace();
1022 stream << "QuadPath(" << path.elementCount() << " main elements, "
1023 << path.elementCountRecursive() << " leaf elements, "
1024 << (path.fillRule() == Qt::OddEvenFill ? "OddEven" : "Winding")
1025 << ", hints: " << path.pathHints() << Qt::endl;
1026 int count = 0;
1027 path.iterateElements([&](const QQuadPath::Element &e, int) {
1028 stream << " " << count++ << (e.isSubpathStart() ? ">" : e.isSubpathEnd() ? "<" : " ");
1029 printElement(stream, e);
1030 stream << Qt::endl;
1031 });
1032 stream << ")";
1033 return stream;
1034}
1035#endif
1036
1037QT_END_NAMESPACE
bool consume(float length)
float lastLength()
QVector2D currentControlPoint()
QVector2D currentCutPoint()
ElementCutter(const QQuadPath::Element &element)
Combined button and popup list for selecting options.
QDebug operator<<(QDebug dbg, const QFileInfo &fi)
static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff)
static QPointF qt_quadraticForCubic(const QBezier &b)
Definition qquadpath.cpp:76
static float crossProduct(const QVector2D &sp, const QVector2D &p, const QVector2D &ep)
static void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit=0.01)
static QT_BEGIN_NAMESPACE qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp)
Definition qquadpath.cpp:15
static void printElement(QDebug stream, const QQuadPath::Element &element)
static bool hasOverlappingLines(const QQuadPath &path)
#define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN
static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
Definition qquadpath.cpp:97