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 "qquickshapecurverenderer_p.h"
5#include "qquickshapecurverenderer_p_p.h"
6#include "qquickshapecurvenode_p.h"
7#include "qquickshapestrokenode_p.h"
8
9#include <QtGui/qvector2d.h>
10#include <QtGui/qvector4d.h>
11#include <QtGui/private/qtriangulator_p.h>
12#include <QtGui/private/qtriangulatingstroker_p.h>
13#include <QtGui/private/qrhi_p.h>
14
15#include <QtQuick/qsgmaterial.h>
16
17#include <QThread>
18
19QT_BEGIN_NAMESPACE
20
21Q_LOGGING_CATEGORY(lcShapeCurveRenderer, "qt.shape.curverenderer");
22
23namespace {
24
25class QQuickShapeWireFrameMaterialShader : public QSGMaterialShader
26{
27public:
28 QQuickShapeWireFrameMaterialShader()
29 {
30 setShaderFileName(stage: VertexStage,
31 QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.vert.qsb"));
32 setShaderFileName(stage: FragmentStage,
33 QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.frag.qsb"));
34 }
35
36 bool updateUniformData(RenderState &state, QSGMaterial *, QSGMaterial *) override
37 {
38 bool changed = false;
39 QByteArray *buf = state.uniformData();
40 Q_ASSERT(buf->size() >= 64);
41
42 if (state.isMatrixDirty()) {
43 const QMatrix4x4 m = state.combinedMatrix();
44
45 memcpy(dest: buf->data(), src: m.constData(), n: 64);
46 changed = true;
47 }
48
49 return changed;
50 }
51};
52
53class QQuickShapeWireFrameMaterial : public QSGMaterial
54{
55public:
56 QQuickShapeWireFrameMaterial()
57 {
58 setFlag(flags: Blending, on: true);
59 }
60
61 int compare(const QSGMaterial *other) const override
62 {
63 return (type() - other->type());
64 }
65
66protected:
67 QSGMaterialType *type() const override
68 {
69 static QSGMaterialType t;
70 return &t;
71 }
72 QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override
73 {
74 return new QQuickShapeWireFrameMaterialShader;
75 }
76
77};
78
79class QQuickShapeWireFrameNode : public QSGGeometryNode
80{
81public:
82 struct WireFrameVertex
83 {
84 float x, y, u, v, w;
85 };
86
87 QQuickShapeWireFrameNode()
88 {
89 setFlag(OwnsGeometry, true);
90 setGeometry(new QSGGeometry(attributes(), 0, 0));
91 activateMaterial();
92 }
93
94 void activateMaterial()
95 {
96 m_material.reset(other: new QQuickShapeWireFrameMaterial);
97 setMaterial(m_material.data());
98 }
99
100 static const QSGGeometry::AttributeSet &attributes()
101 {
102 static QSGGeometry::Attribute data[] = {
103 QSGGeometry::Attribute::createWithAttributeType(pos: 0, tupleSize: 2, primitiveType: QSGGeometry::FloatType, attributeType: QSGGeometry::PositionAttribute),
104 QSGGeometry::Attribute::createWithAttributeType(pos: 1, tupleSize: 3, primitiveType: QSGGeometry::FloatType, attributeType: QSGGeometry::TexCoordAttribute),
105 };
106 static QSGGeometry::AttributeSet attrs = { .count: 2, .stride: sizeof(WireFrameVertex), .attributes: data };
107 return attrs;
108 }
109
110protected:
111 QScopedPointer<QQuickShapeWireFrameMaterial> m_material;
112};
113}
114
115
116QQuickShapeCurveRenderer::~QQuickShapeCurveRenderer() { }
117
118void QQuickShapeCurveRenderer::beginSync(int totalCount, bool *countChanged)
119{
120 if (countChanged != nullptr && totalCount != m_paths.size())
121 *countChanged = true;
122 m_paths.resize(size: totalCount);
123}
124
125void QQuickShapeCurveRenderer::setPath(int index, const QQuickPath *path)
126{
127 auto &pathData = m_paths[index];
128 pathData.originalPath = path->path();
129 pathData.m_dirty |= PathDirty;
130}
131
132void QQuickShapeCurveRenderer::setStrokeColor(int index, const QColor &color)
133{
134 auto &pathData = m_paths[index];
135 const bool wasVisible = pathData.isStrokeVisible();
136 pathData.pen.setColor(color);
137 if (pathData.isStrokeVisible() != wasVisible)
138 pathData.m_dirty |= StrokeDirty;
139 else
140 pathData.m_dirty |= UniformsDirty;
141}
142
143void QQuickShapeCurveRenderer::setStrokeWidth(int index, qreal w)
144{
145 auto &pathData = m_paths[index];
146 if (w > 0) {
147 pathData.validPenWidth = true;
148 pathData.pen.setWidthF(w);
149 } else {
150 pathData.validPenWidth = false;
151 }
152 pathData.m_dirty |= StrokeDirty;
153}
154
155void QQuickShapeCurveRenderer::setFillColor(int index, const QColor &color)
156{
157 auto &pathData = m_paths[index];
158 const bool wasVisible = pathData.isFillVisible();
159 pathData.fillColor = color;
160 if (pathData.isFillVisible() != wasVisible)
161 pathData.m_dirty |= FillDirty;
162 else
163 pathData.m_dirty |= UniformsDirty;
164}
165
166void QQuickShapeCurveRenderer::setFillRule(int index, QQuickShapePath::FillRule fillRule)
167{
168 auto &pathData = m_paths[index];
169 pathData.fillRule = Qt::FillRule(fillRule);
170 pathData.m_dirty |= PathDirty;
171}
172
173void QQuickShapeCurveRenderer::setJoinStyle(int index,
174 QQuickShapePath::JoinStyle joinStyle,
175 int miterLimit)
176{
177 auto &pathData = m_paths[index];
178 pathData.pen.setJoinStyle(Qt::PenJoinStyle(joinStyle));
179 pathData.pen.setMiterLimit(miterLimit);
180 pathData.m_dirty |= StrokeDirty;
181}
182
183void QQuickShapeCurveRenderer::setCapStyle(int index, QQuickShapePath::CapStyle capStyle)
184{
185 auto &pathData = m_paths[index];
186 pathData.pen.setCapStyle(Qt::PenCapStyle(capStyle));
187 pathData.m_dirty |= StrokeDirty;
188}
189
190void QQuickShapeCurveRenderer::setStrokeStyle(int index,
191 QQuickShapePath::StrokeStyle strokeStyle,
192 qreal dashOffset,
193 const QVector<qreal> &dashPattern)
194{
195 auto &pathData = m_paths[index];
196 pathData.pen.setStyle(Qt::PenStyle(strokeStyle));
197 if (strokeStyle == QQuickShapePath::DashLine) {
198 pathData.pen.setDashPattern(dashPattern);
199 pathData.pen.setDashOffset(dashOffset);
200 }
201 pathData.m_dirty |= StrokeDirty;
202}
203
204void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *gradient)
205{
206 PathData &pd(m_paths[index]);
207 pd.gradientType = NoGradient;
208 if (QQuickShapeLinearGradient *g = qobject_cast<QQuickShapeLinearGradient *>(object: gradient)) {
209 pd.gradientType = LinearGradient;
210 pd.gradient.stops = gradient->gradientStops();
211 pd.gradient.spread = gradient->spread();
212 pd.gradient.a = QPointF(g->x1(), g->y1());
213 pd.gradient.b = QPointF(g->x2(), g->y2());
214 } else if (QQuickShapeRadialGradient *g = qobject_cast<QQuickShapeRadialGradient *>(object: gradient)) {
215 pd.gradientType = RadialGradient;
216 pd.gradient.a = QPointF(g->centerX(), g->centerY());
217 pd.gradient.b = QPointF(g->focalX(), g->focalY());
218 pd.gradient.v0 = g->centerRadius();
219 pd.gradient.v1 = g->focalRadius();
220 } else if (QQuickShapeConicalGradient *g = qobject_cast<QQuickShapeConicalGradient *>(object: gradient)) {
221 pd.gradientType = ConicalGradient;
222 pd.gradient.a = QPointF(g->centerX(), g->centerY());
223 pd.gradient.v0 = g->angle();
224 } else
225 if (gradient != nullptr) {
226 static bool warned = false;
227 if (!warned) {
228 warned = true;
229 qCWarning(lcShapeCurveRenderer) << "Unsupported gradient fill";
230 }
231 }
232
233 if (pd.gradientType != NoGradient) {
234 pd.gradient.stops = gradient->gradientStops();
235 pd.gradient.spread = gradient->spread();
236 }
237
238 pd.m_dirty |= FillDirty;
239}
240
241void QQuickShapeCurveRenderer::setAsyncCallback(void (*callback)(void *), void *data)
242{
243 qCWarning(lcShapeCurveRenderer) << "Asynchronous creation not supported by CurveRenderer";
244 Q_UNUSED(callback);
245 Q_UNUSED(data);
246}
247
248void QQuickShapeCurveRenderer::endSync(bool async)
249{
250 Q_UNUSED(async);
251}
252
253void QQuickShapeCurveRenderer::updateNode()
254{
255 if (!m_rootNode)
256 return;
257 static const bool doOverlapSolving = !qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_DISABLE_OVERLAP_SOLVER");
258 static const bool useTriangulatingStroker = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_TRIANGULATING_STROKER");
259 static const bool simplifyPath = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_SIMPLIFY_PATHS");
260
261 for (PathData &pathData : m_paths) {
262 int dirtyFlags = pathData.m_dirty;
263
264 if (dirtyFlags & PathDirty) {
265 if (simplifyPath)
266 pathData.path = QQuadPath::fromPainterPath(path: pathData.originalPath.simplified());
267 else
268 pathData.path = QQuadPath::fromPainterPath(path: pathData.originalPath);
269 pathData.path.setFillRule(pathData.fillRule);
270 pathData.fillPath = {};
271 dirtyFlags |= (FillDirty | StrokeDirty);
272 }
273
274 if (dirtyFlags & FillDirty) {
275 deleteAndClear(nodeList: &pathData.fillNodes);
276 deleteAndClear(nodeList: &pathData.fillDebugNodes);
277 if (pathData.isFillVisible()) {
278 if (pathData.fillPath.isEmpty()) {
279 pathData.fillPath = pathData.path.subPathsClosed();
280 pathData.fillPath.addCurvatureData();
281 if (doOverlapSolving)
282 solveOverlaps(path&: pathData.fillPath);
283 }
284 pathData.fillNodes = addFillNodes(pathData, debugNodes: &pathData.fillDebugNodes);
285 dirtyFlags |= StrokeDirty;
286 }
287 }
288
289 if (dirtyFlags & StrokeDirty) {
290 deleteAndClear(nodeList: &pathData.strokeNodes);
291 deleteAndClear(nodeList: &pathData.strokeDebugNodes);
292 if (pathData.isStrokeVisible()) {
293 const QPen &pen = pathData.pen;
294 if (pen.style() == Qt::SolidLine)
295 pathData.strokePath = pathData.path;
296 else
297 pathData.strokePath = pathData.path.dashed(lineWidth: pen.widthF(), dashPattern: pen.dashPattern(), dashOffset: pen.dashOffset());
298
299 if (useTriangulatingStroker)
300 pathData.strokeNodes = addTriangulatingStrokerNodes(pathData, debugNodes: &pathData.strokeDebugNodes);
301 else
302 pathData.strokeNodes = addCurveStrokeNodes(pathData, debugNodes: &pathData.strokeDebugNodes);
303 }
304 }
305
306 if (dirtyFlags & UniformsDirty) {
307 if (!(dirtyFlags & FillDirty)) {
308 for (auto &pathNode : std::as_const(t&: pathData.fillNodes))
309 static_cast<QQuickShapeCurveNode *>(pathNode)->setColor(pathData.fillColor);
310 }
311 if (!(dirtyFlags & StrokeDirty)) {
312 if (useTriangulatingStroker) {
313 for (auto &strokeNode : std::as_const(t&: pathData.strokeNodes))
314 static_cast<QQuickShapeCurveNode *>(strokeNode)->setColor(pathData.pen.color());
315 } else {
316 for (auto &strokeNode : std::as_const(t&: pathData.strokeNodes))
317 static_cast<QQuickShapeStrokeNode *>(strokeNode)->setColor(pathData.pen.color());
318 }
319 }
320 }
321
322 pathData.m_dirty &= ~(PathDirty | FillDirty | StrokeDirty | UniformsDirty);
323 }
324}
325
326namespace {
327// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space.
328// 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)
329static inline QVector2D uvForPoint(QVector2D v1, QVector2D v2, QVector2D p)
330{
331 double divisor = v1.x() * v2.y() - v2.x() * v1.y();
332
333 float u = (p.x() * v2.y() - p.y() * v2.x()) / divisor;
334 float v = (p.y() * v1.x() - p.x() * v1.y()) / divisor;
335
336 return {u, v};
337}
338
339// Find uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1
340// also works for a line from p0 to p2, where p1 is on the inside of the path relative to the line
341static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p)
342{
343 QVector2D v1 = 2 * (p1 - p0);
344 QVector2D v2 = p2 - v1 - p0;
345 return uvForPoint(v1, v2, p: p - p0);
346}
347
348static QVector3D elementUvForPoint(const QQuadPath::Element& e, QVector2D p)
349{
350 auto uv = curveUv(p0: e.startPoint(), p1: e.controlPoint(), p2: e.endPoint(), p);
351 if (e.isLine())
352 return { uv.x(), uv.y(), 0.0f };
353 else
354 return { uv.x(), uv.y(), e.isConvex() ? -1.0f : 1.0f };
355}
356
357static inline QVector2D calcNormalVector(QVector2D a, QVector2D b)
358{
359 auto v = b - a;
360 return {v.y(), -v.x()};
361}
362
363// The sign of the return value indicates which side of the line defined by a and n the point p falls
364static inline float testSideOfLineByNormal(QVector2D a, QVector2D n, QVector2D p)
365{
366 float dot = QVector2D::dotProduct(v1: p - a, v2: n);
367 return dot;
368};
369
370template<typename Func>
371void iteratePath(const QQuadPath &path, int index, Func &&lambda)
372{
373 const auto &element = path.elementAt(i: index);
374 if (element.childCount() == 0) {
375 lambda(element, index);
376 } else {
377 for (int i = 0; i < element.childCount(); ++i)
378 iteratePath(path, element.indexOfChild(childNumber: i), lambda);
379 }
380}
381
382static inline float determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
383{
384 return p1.x() * (p2.y() - p3.y())
385 + p2.x() * (p3.y() - p1.y())
386 + p3.x() * (p1.y() - p2.y());
387}
388}
389
390QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addFillNodes(const PathData &pathData,
391 NodeList *debugNodes)
392{
393 auto *node = new QQuickShapeCurveNode;
394 node->setGradientType(pathData.gradientType);
395
396 QVector<QSGGeometryNode *> ret;
397 const QColor &color = pathData.fillColor;
398 QPainterPath internalHull;
399 internalHull.setFillRule(pathData.fillPath.fillRule());
400
401 bool visualizeDebug = debugVisualization() & DebugCurves;
402 const float dbg = visualizeDebug ? 0.5f : 0.0f;
403 node->setDebug(dbg);
404 QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
405
406 QHash<QPair<float, float>, int> linePointHash;
407 QHash<QPair<float, float>, int> concaveControlPointHash;
408 QHash<QPair<float, float>, int> convexPointHash;
409
410 auto toRoundedPair = [](const QPointF &p) -> QPair<float, float> {
411 return qMakePair(value1: qRound(d: p.x() * 32.0f) / 32.0f, value2: qRound(d: p.y() * 32.0f) / 32.0f);
412 };
413
414 auto toRoundedVec2D = [](const QPointF &p) -> QVector2D {
415 return { qRound(d: p.x() * 32.0f) / 32.0f, qRound(d: p.y() * 32.0f) / 32.0f };
416 };
417
418 auto roundVec2D = [](const QVector2D &p) -> QVector2D {
419 return { qRound(f: p.x() * 32.0f) / 32.0f, qRound(f: p.y() * 32.0f) / 32.0f };
420 };
421
422 auto addCurveTriangle = [&](const QQuadPath::Element &element,
423 const QVector2D &sp,
424 const QVector2D &ep,
425 const QVector2D &cp) {
426 node->appendTriangle(v1: sp, v2: cp, v3: ep,
427 uvForPoint: [&element](QVector2D v) { return elementUvForPoint(e: element, p: v); });
428
429 wfVertices.append(t: {.x: sp.x(), .y: sp.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); // 0
430 wfVertices.append(t: {.x: cp.x(), .y: cp.y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); // 1
431 wfVertices.append(t: {.x: ep.x(), .y: ep.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); // 2
432 };
433
434 auto addCurveTriangleWithNormals = [&](const QQuadPath::Element &element,
435 const std::array<QVector2D, 3> &v,
436 const std::array<QVector2D, 3> &n) {
437 node->appendTriangle(v, n, uvForPoint: [&element](QVector2D v) { return elementUvForPoint(e: element, p: v); });
438 wfVertices.append(t: {.x: v[0].x(), .y: v[0].y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); // 0
439 wfVertices.append(t: {.x: v[1].x(), .y: v[1].y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); // 1
440 wfVertices.append(t: {.x: v[2].x(), .y: v[2].y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); // 2
441 };
442
443 auto outsideNormal = [](const QVector2D &startPoint,
444 const QVector2D &endPoint,
445 const QVector2D &insidePoint) {
446
447 QVector2D baseLine = endPoint - startPoint;
448 QVector2D insideVector = insidePoint - startPoint;
449 QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
450
451 bool swap = QVector2D::dotProduct(v1: insideVector, v2: normal) < 0;
452
453 return swap ? normal : -normal;
454 };
455
456 auto addTriangleForLine = [&](const QQuadPath::Element &element,
457 const QVector2D &sp,
458 const QVector2D &ep,
459 const QVector2D &cp) {
460 addCurveTriangle(element, sp, ep, cp);
461
462 // Add triangles on the outer side to make room for AA
463 const QVector2D normal = outsideNormal(sp, ep, cp);
464 constexpr QVector2D null;
465 addCurveTriangleWithNormals(element, {sp, sp, ep}, {null, normal, null});
466 addCurveTriangleWithNormals(element, {sp, ep, ep}, {normal, normal, null});
467 };
468
469 auto addTriangleForConcave = [&](const QQuadPath::Element &element,
470 const QVector2D &sp,
471 const QVector2D &ep,
472 const QVector2D &cp) {
473 addTriangleForLine(element, sp, ep, cp);
474 };
475
476 auto addTriangleForConvex = [&](const QQuadPath::Element &element,
477 const QVector2D &sp,
478 const QVector2D &ep,
479 const QVector2D &cp) {
480 addCurveTriangle(element, sp, ep, cp);
481 // Add two triangles on the outer side to get some more AA
482
483 constexpr QVector2D null;
484 // First triangle on the line sp-cp, replacing ep
485 {
486 const QVector2D normal = outsideNormal(sp, cp, ep);
487 addCurveTriangleWithNormals(element, {sp, sp, cp}, {null, normal, null});
488 }
489
490 // Second triangle on the line ep-cp, replacing sp
491 {
492 const QVector2D normal = outsideNormal(ep, cp, sp);
493 addCurveTriangleWithNormals(element, {ep, ep, cp}, {null, normal, null});
494 }
495 };
496
497 auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3) {
498 constexpr QVector3D uv(0.0, 1.0, -1.0);
499 node->appendTriangle(v1: p1, v2: p2, v3: p3, uvForPoint: [&uv](QVector2D) { return uv; } );
500
501 wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); // 0
502 wfVertices.append(t: {.x: p3.x(), .y: p3.y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); // 1
503 wfVertices.append(t: {.x: p2.x(), .y: p2.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); // 2
504 };
505
506 for (int i = 0; i < pathData.fillPath.elementCount(); ++i) {
507 iteratePath(path: pathData.fillPath, index: i, lambda: [&](const QQuadPath::Element &element, int index) {
508 QPointF sp(element.startPoint().toPointF()); //### to much conversion to and from pointF
509 QPointF cp(element.controlPoint().toPointF());
510 QPointF ep(element.endPoint().toPointF());
511 if (element.isSubpathStart())
512 internalHull.moveTo(p: sp);
513 if (element.isLine()) {
514 internalHull.lineTo(p: ep);
515 linePointHash.insert(key: toRoundedPair(sp), value: index);
516 } else {
517 if (element.isConvex()) {
518 internalHull.lineTo(p: ep);
519 addTriangleForConvex(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp));
520 convexPointHash.insert(key: toRoundedPair(sp), value: index);
521 } else {
522 internalHull.lineTo(p: cp);
523 internalHull.lineTo(p: ep);
524 addTriangleForConcave(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp));
525 concaveControlPointHash.insert(key: toRoundedPair(cp), value: index);
526 }
527 }
528 });
529 }
530
531 auto makeHashable = [](const QVector2D &p) -> QPair<float, float> {
532 return qMakePair(value1: qRound(f: p.x() * 32.0f) / 32.0f, value2: qRound(f: p.y() * 32.0f) / 32.0f);
533 };
534 // Points in p are already rounded do 1/32
535 // Returns false if the triangle needs to be split. Adds the triangle to the graphics buffers and returns true otherwise.
536 // (Does not handle ambiguous vertices that are on multiple unrelated lines/curves)
537 auto onSameSideOfLine = [](const QVector2D &p1,
538 const QVector2D &p2,
539 const QVector2D &linePoint,
540 const QVector2D &lineNormal) {
541 float side1 = testSideOfLineByNormal(a: linePoint, n: lineNormal, p: p1);
542 float side2 = testSideOfLineByNormal(a: linePoint, n: lineNormal, p: p2);
543 return side1 * side2 >= 0;
544 };
545
546 auto pointInSafeSpace = [&](const QVector2D &p, const QQuadPath::Element &element) -> bool {
547 const QVector2D a = element.startPoint();
548 const QVector2D b = element.endPoint();
549 const QVector2D c = element.controlPoint();
550 // There are "safe" areas of the curve also across the baseline: the curve can never cross:
551 // line1: the line through A and B'
552 // line2: the line through B and A'
553 // Where A' = A "mirrored" through C and B' = B "mirrored" through C
554 const QVector2D n1 = calcNormalVector(a, b: c + (c - b));
555 const QVector2D n2 = calcNormalVector(a: b, b: c + (c - a));
556 bool safeSideOf1 = onSameSideOfLine(p, c, a, n1);
557 bool safeSideOf2 = onSameSideOfLine(p, c, b, n2);
558 return safeSideOf1 && safeSideOf2;
559 };
560
561 auto handleTriangle = [&](const QVector2D (&p)[3]) -> bool {
562 int lineElementIndex = -1;
563 int concaveElementIndex = -1;
564 int convexElementIndex = -1;
565
566 bool foundElement = false;
567 int si = -1;
568 int ei = -1;
569 for (int i = 0; i < 3; ++i) {
570 if (auto found = linePointHash.constFind(key: makeHashable(p[i])); found != linePointHash.constEnd()) {
571 // 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
572 const auto &element = pathData.fillPath.elementAt(i: *found);
573 for (int j = 0; j < 3; ++j) {
574 if (i != j && roundVec2D(element.endPoint()) == p[j]) {
575 if (foundElement)
576 return false; // More than one edge on path: must split
577 lineElementIndex = *found;
578 si = i;
579 ei = j;
580 foundElement = true;
581 }
582 }
583 } else if (auto found = concaveControlPointHash.constFind(key: makeHashable(p[i])); found != concaveControlPointHash.constEnd()) {
584 // check if this triangle is on the tangent line of a concave curve,
585 // i.e if one point is the cp, and the other is sp or ep
586 // TODO: clean up duplicated code (almost the same as the lineElement path above)
587 const auto &element = pathData.fillPath.elementAt(i: *found);
588 for (int j = 0; j < 3; ++j) {
589 if (i == j)
590 continue;
591 if (roundVec2D(element.startPoint()) == p[j] || roundVec2D(element.endPoint()) == p[j]) {
592 if (foundElement)
593 return false; // More than one edge on path: must split
594 concaveElementIndex = *found;
595 // The tangent line is p[i] - p[j]
596 si = i;
597 ei = j;
598 foundElement = true;
599 }
600 }
601 } else if (auto found = convexPointHash.constFind(key: makeHashable(p[i])); found != convexPointHash.constEnd()) {
602 // check if this triangle is on a curve, i.e. if one point is the sp and another is the ep of the same path element
603 const auto &element = pathData.fillPath.elementAt(i: *found);
604 for (int j = 0; j < 3; ++j) {
605 if (i != j && roundVec2D(element.endPoint()) == p[j]) {
606 if (foundElement)
607 return false; // More than one edge on path: must split
608 convexElementIndex = *found;
609 si = i;
610 ei = j;
611 foundElement = true;
612 }
613 }
614 }
615 }
616 if (lineElementIndex != -1) {
617 int ci = (6 - si - ei) % 3; // 1+2+3 is 6, so missing number is 6-n1-n2
618 addTriangleForLine(pathData.fillPath.elementAt(i: lineElementIndex), p[si], p[ei], p[ci]);
619 } else if (concaveElementIndex != -1) {
620 addCurveTriangle(pathData.fillPath.elementAt(i: concaveElementIndex), p[0], p[1], p[2]);
621 } else if (convexElementIndex != -1) {
622 int oi = (6 - si - ei) % 3;
623 const auto &otherPoint = p[oi];
624 const auto &element = pathData.fillPath.elementAt(i: convexElementIndex);
625 // We have to test whether the triangle can cross the line
626 // TODO: use the toplevel element's safe space
627 bool safeSpace = pointInSafeSpace(otherPoint, element);
628 if (safeSpace) {
629 addCurveTriangle(element, p[0], p[1], p[2]);
630 } else {
631 // Find a point inside the triangle that's also in the safe space
632 QVector2D newPoint = (p[0] + p[1] + p[2]) / 3;
633 // We should calculate the point directly, but just do a lazy implementation for now:
634 for (int i = 0; i < 7; ++i) {
635 safeSpace = pointInSafeSpace(newPoint, element);
636 if (safeSpace)
637 break;
638 newPoint = (p[si] + p[ei] + newPoint) / 3;
639 }
640 if (safeSpace) {
641 // Split triangle. We know the original triangle is only on one path element, so the other triangles are both fill.
642 // Curve triangle is (sp, ep, np)
643 addCurveTriangle(element, p[si], p[ei], newPoint);
644 // The other two are (sp, op, np) and (ep, op, np)
645 addFillTriangle(p[si], p[oi], newPoint);
646 addFillTriangle(p[ei], p[oi], newPoint);
647 } else {
648 // fallback to fill if we can't find a point in safe space
649 addFillTriangle(p[0], p[1], p[2]);
650 }
651 }
652
653 } else {
654 addFillTriangle(p[0], p[1], p[2]);
655 }
656 return true;
657 };
658
659 QTriangleSet triangles = qTriangulate(path: internalHull);
660
661 const quint32 *idxTable = static_cast<const quint32 *>(triangles.indices.data());
662 for (int triangle = 0; triangle < triangles.indices.size() / 3; ++triangle) {
663 const quint32 *idx = &idxTable[triangle * 3];
664
665 QVector2D p[3];
666 for (int i = 0; i < 3; ++i) {
667 p[i] = toRoundedVec2D(QPointF(triangles.vertices.at(i: idx[i] * 2),
668 triangles.vertices.at(i: idx[i] * 2 + 1)));
669 }
670 if (qFuzzyIsNull(f: determinant(p1: p[0], p2: p[1], p3: p[2])))
671 continue; // Skip degenerate triangles
672 bool needsSplit = !handleTriangle(p);
673 if (needsSplit) {
674 QVector2D c = (p[0] + p[1] + p[2]) / 3;
675 for (int i = 0; i < 3; ++i) {
676 qSwap(value1&: c, value2&: p[i]);
677 handleTriangle(p);
678 qSwap(value1&: c, value2&: p[i]);
679 }
680 }
681 }
682
683 QVector<quint32> indices = node->uncookedIndexes();
684 if (indices.size() > 0) {
685 node->setColor(color);
686 node->setFillGradient(pathData.gradient);
687
688 node->cookGeometry();
689 m_rootNode->appendChildNode(node);
690 ret.append(t: node);
691 }
692
693 const bool wireFrame = debugVisualization() & DebugWireframe;
694 if (wireFrame) {
695 QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
696 QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
697 wfVertices.size(),
698 indices.size(),
699 QSGGeometry::UnsignedIntType);
700 wfNode->setGeometry(wfg);
701
702 wfg->setDrawingMode(QSGGeometry::DrawTriangles);
703 memcpy(dest: wfg->indexData(),
704 src: indices.data(),
705 n: indices.size() * wfg->sizeOfIndex());
706 memcpy(dest: wfg->vertexData(),
707 src: wfVertices.data(),
708 n: wfg->vertexCount() * wfg->sizeOfVertex());
709
710 m_rootNode->appendChildNode(node: wfNode);
711 debugNodes->append(t: wfNode);
712 }
713
714 return ret;
715}
716
717QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addTriangulatingStrokerNodes(const PathData &pathData, NodeList *debugNodes)
718{
719 QVector<QSGGeometryNode *> ret;
720 const QColor &color = pathData.pen.color();
721
722 QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
723
724 QTriangulatingStroker stroker;
725 const auto painterPath = pathData.strokePath.toPainterPath();
726 const QVectorPath &vp = qtVectorPathForPath(path: painterPath);
727 QPen pen = pathData.pen;
728 stroker.process(path: vp, pen, clip: {}, hints: {});
729
730 auto *node = new QQuickShapeCurveNode;
731 node->setGradientType(pathData.gradientType);
732
733 auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){
734
735 QVector2D baseLine = endPoint - startPoint;
736 QVector2D insideVector = referencePoint - startPoint;
737 QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()); // TODO: limit size of triangle
738
739 bool swap = QVector2D::dotProduct(v1: insideVector, v2: normal) < 0;
740
741 return swap ? startPoint + normal : startPoint - normal;
742 };
743
744 static bool disableExtraTriangles = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_WIP_DISABLE_EXTRA_STROKE_TRIANGLES");
745
746 auto addStrokeTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, bool){
747 if (p1 == p2 || p2 == p3) {
748 return;
749 }
750
751 auto uvForPoint = [&p1, &p2, &p3](QVector2D p) {
752 auto uv = curveUv(p0: p1, p1: p2, p2: p3, p);
753 return QVector3D(uv.x(), uv.y(), 0.0f); // Line
754 };
755
756 node->appendTriangle(v1: p1, v2: p2, v3: p3, uvForPoint);
757
758
759 wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); // 0
760 wfVertices.append(t: {.x: p2.x(), .y: p2.y(), .u: 0.0f, .v: 0.1f, .w: 0.0f}); // 1
761 wfVertices.append(t: {.x: p3.x(), .y: p3.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); // 2
762
763 if (!disableExtraTriangles) {
764 // Add a triangle on the outer side of the line to get some more AA
765 // The new point replaces p2 (currentVertex+1)
766 QVector2D op = findPointOtherSide(p1, p3, p2);
767 node->appendTriangle(v1: p1, v2: op, v3: p3, uvForPoint);
768
769 wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f});
770 wfVertices.append(t: {.x: op.x(), .y: op.y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); // replacing p2
771 wfVertices.append(t: {.x: p3.x(), .y: p3.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f});
772 }
773 };
774
775 const int vertCount = stroker.vertexCount() / 2;
776 const float *verts = stroker.vertices();
777 for (int i = 0; i < vertCount - 2; ++i) {
778 QVector2D p[3];
779 for (int j = 0; j < 3; ++j) {
780 p[j] = QVector2D(verts[(i+j)*2], verts[(i+j)*2 + 1]);
781 }
782 bool isOdd = i % 2;
783 addStrokeTriangle(p[0], p[1], p[2], isOdd);
784 }
785
786 QVector<quint32> indices = node->uncookedIndexes();
787 if (indices.size() > 0) {
788 node->setColor(color);
789 node->setFillGradient(pathData.gradient);
790
791 node->cookGeometry();
792 m_rootNode->appendChildNode(node);
793 ret.append(t: node);
794 }
795 const bool wireFrame = debugVisualization() & DebugWireframe;
796 if (wireFrame) {
797 QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
798 QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
799 wfVertices.size(),
800 indices.size(),
801 QSGGeometry::UnsignedIntType);
802 wfNode->setGeometry(wfg);
803
804 wfg->setDrawingMode(QSGGeometry::DrawTriangles);
805 memcpy(dest: wfg->indexData(),
806 src: indices.data(),
807 n: indices.size() * wfg->sizeOfIndex());
808 memcpy(dest: wfg->vertexData(),
809 src: wfVertices.data(),
810 n: wfg->vertexCount() * wfg->sizeOfVertex());
811
812 m_rootNode->appendChildNode(node: wfNode);
813 debugNodes->append(t: wfNode);
814 }
815
816 return ret;
817}
818
819void QQuickShapeCurveRenderer::setRootNode(QSGNode *node)
820{
821 m_rootNode = node;
822}
823
824int QQuickShapeCurveRenderer::debugVisualizationFlags = QQuickShapeCurveRenderer::NoDebug;
825
826int QQuickShapeCurveRenderer::debugVisualization()
827{
828 static const int envFlags = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_DEBUG");
829 return debugVisualizationFlags | envFlags;
830}
831
832void QQuickShapeCurveRenderer::setDebugVisualization(int options)
833{
834 if (debugVisualizationFlags == options)
835 return;
836 debugVisualizationFlags = options;
837}
838
839void QQuickShapeCurveRenderer::deleteAndClear(NodeList *nodeList)
840{
841 for (QSGNode *node : std::as_const(t&: *nodeList))
842 delete node;
843 nodeList->clear();
844}
845
846namespace {
847
848/*
849 Clever triangle overlap algorithm. Stack Overflow says:
850
851 You can prove that the two triangles do not collide by finding an edge (out of the total 6
852 edges that make up the two triangles) that acts as a separating line where all the vertices
853 of one triangle lie on one side and the vertices of the other triangle lie on the other side.
854 If you can find such an edge then it means that the triangles do not intersect otherwise the
855 triangles are colliding.
856*/
857using TrianglePoints = std::array<QVector2D, 3>;
858using LinePoints = std::array<QVector2D, 2>;
859
860// The sign of the determinant tells the winding order: positive means counter-clockwise
861
862static inline double determinant(const TrianglePoints &p)
863{
864 return determinant(p1: p[0], p2: p[1], p3: p[2]);
865}
866
867// Fix the triangle so that the determinant is positive
868static void fixWinding(TrianglePoints &p)
869{
870 double det = determinant(p);
871 if (det < 0.0) {
872 qSwap(value1&: p[0], value2&: p[1]);
873 }
874}
875
876// Return true if the determinant is negative, i.e. if the winding order is opposite of the triangle p1,p2,p3.
877// This means that p is strictly on the other side of p1-p2 relative to p3 [where p1,p2,p3 is a triangle with
878// a positive determinant].
879bool checkEdge(QVector2D &p1, QVector2D &p2, QVector2D &p, float epsilon)
880{
881 return determinant(p1, p2, p3: p) <= epsilon;
882}
883
884
885bool checkTriangleOverlap(TrianglePoints &triangle1, TrianglePoints &triangle2, float epsilon = 1.0/32)
886{
887 // See if there is an edge of triangle1 such that all vertices in triangle2 are on the opposite side
888 fixWinding(p&: triangle1);
889 for (int i = 0; i < 3; i++) {
890 int ni = (i + 1) % 3;
891 if (checkEdge(p1&: triangle1[i], p2&: triangle1[ni], p&: triangle2[0], epsilon) &&
892 checkEdge(p1&: triangle1[i], p2&: triangle1[ni], p&: triangle2[1], epsilon) &&
893 checkEdge(p1&: triangle1[i], p2&: triangle1[ni], p&: triangle2[2], epsilon))
894 return false;
895 }
896
897 // See if there is an edge of triangle2 such that all vertices in triangle1 are on the opposite side
898 fixWinding(p&: triangle2);
899 for (int i = 0; i < 3; i++) {
900 int ni = (i + 1) % 3;
901
902 if (checkEdge(p1&: triangle2[i], p2&: triangle2[ni], p&: triangle1[0], epsilon) &&
903 checkEdge(p1&: triangle2[i], p2&: triangle2[ni], p&: triangle1[1], epsilon) &&
904 checkEdge(p1&: triangle2[i], p2&: triangle2[ni], p&: triangle1[2], epsilon))
905 return false;
906 }
907
908 return true;
909}
910
911bool checkLineTriangleOverlap(TrianglePoints &triangle, LinePoints &line, float epsilon = 1.0/32)
912{
913 // See if all vertices of the triangle are on the same side of the line
914 bool s1 = determinant(p1: line[0], p2: line[1], p3: triangle[0]) < 0;
915 auto s2 = determinant(p1: line[0], p2: line[1], p3: triangle[1]) < 0;
916 auto s3 = determinant(p1: line[0], p2: line[1], p3: triangle[2]) < 0;
917 // If all determinants have the same sign, then there is no overlap
918 if (s1 == s2 && s2 == s3) {
919 return false;
920 }
921 // See if there is an edge of triangle1 such that both vertices in line are on the opposite side
922 fixWinding(p&: triangle);
923 for (int i = 0; i < 3; i++) {
924 int ni = (i + 1) % 3;
925 if (checkEdge(p1&: triangle[i], p2&: triangle[ni], p&: line[0], epsilon) &&
926 checkEdge(p1&: triangle[i], p2&: triangle[ni], p&: line[1], epsilon))
927 return false;
928 }
929
930 return true;
931}
932
933// We could slightly optimize this if we did fixWinding in advance
934bool checkTriangleContains (QVector2D pt, QVector2D v1, QVector2D v2, QVector2D v3, float epsilon = 1.0/32)
935{
936 float d1, d2, d3;
937 d1 = determinant(p1: pt, p2: v1, p3: v2);
938 d2 = determinant(p1: pt, p2: v2, p3: v3);
939 d3 = determinant(p1: pt, p2: v3, p3: v1);
940
941 bool allNegative = d1 < -epsilon && d2 < -epsilon && d3 < -epsilon;
942 bool allPositive = d1 > epsilon && d2 > epsilon && d3 > epsilon;
943
944 return allNegative || allPositive;
945}
946
947// e1 is always a concave curve. e2 can be curve or line
948static bool isOverlap(const QQuadPath &path, int e1, int e2)
949{
950 const QQuadPath::Element &element1 = path.elementAt(i: e1);
951 const QQuadPath::Element &element2 = path.elementAt(i: e2);
952
953 TrianglePoints t1{ element1.startPoint(), element1.controlPoint(), element1.endPoint() };
954
955 if (element2.isLine()) {
956 LinePoints line{ element2.startPoint(), element2.endPoint() };
957 return checkLineTriangleOverlap(triangle&: t1, line);
958 } else {
959 TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() };
960 return checkTriangleOverlap(triangle1&: t1, triangle2&: t2);
961 }
962
963 return false;
964}
965
966static bool isOverlap(const QQuadPath &path, int index, const QVector2D &vertex)
967{
968 const QQuadPath::Element &elem = path.elementAt(i: index);
969 return checkTriangleContains(pt: vertex, v1: elem.startPoint(), v2: elem.controlPoint(), v3: elem.endPoint());
970}
971
972struct TriangleData
973{
974 TrianglePoints points;
975 int pathElementIndex;
976 TrianglePoints normals;
977};
978
979// Returns a vector that is normal to baseLine, pointing to the right
980QVector2D normalVector(QVector2D baseLine)
981{
982 QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
983 return normal;
984}
985
986// Returns a vector that is normal to the path and pointing to the right. If endSide is false
987// the vector is normal to the start point, otherwise to the end point
988QVector2D normalVector(const QQuadPath::Element &element, bool endSide = false)
989{
990 if (element.isLine())
991 return normalVector(baseLine: element.endPoint() - element.startPoint());
992 else if (!endSide)
993 return normalVector(baseLine: element.controlPoint() - element.startPoint());
994 else
995 return normalVector(baseLine: element.endPoint() - element.controlPoint());
996}
997
998// Returns a vector that is parallel to the path. If endSide is false
999// the vector starts at the start point and points forward,
1000// otherwise it starts at the end point and points backward
1001QVector2D tangentVector(const QQuadPath::Element &element, bool endSide = false)
1002{
1003 if (element.isLine()) {
1004 if (!endSide)
1005 return element.endPoint() - element.startPoint();
1006 else
1007 return element.startPoint() - element.endPoint();
1008 } else {
1009 if (!endSide)
1010 return element.controlPoint() - element.startPoint();
1011 else
1012 return element.controlPoint() - element.endPoint();
1013 }
1014}
1015
1016// Really simplistic O(n^2) triangulator - only intended for five points
1017QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, const QList<QVector2D> &normals, int elementIndex)
1018{
1019 int count = pts.size();
1020 Q_ASSERT(count >= 3);
1021 Q_ASSERT(normals.size() == count);
1022
1023 // First we find the convex hull: it's always in positive determinant winding order
1024 QList<int> hull;
1025 float det1 = determinant(p1: pts[0], p2: pts[1], p3: pts[2]);
1026 if (det1 > 0)
1027 hull << 0 << 1 << 2;
1028 else
1029 hull << 2 << 1 << 0;
1030 auto connectableInHull = [&](int idx) -> QList<int> {
1031 QList<int> r;
1032 const int n = hull.size();
1033 const auto &pt = pts[idx];
1034 for (int i = 0; i < n; ++i) {
1035 const auto &i1 = hull.at(i);
1036 const auto &i2 = hull.at(i: (i+1) % n);
1037 if (determinant(p1: pts[i1], p2: pts[i2], p3: pt) < 0.0f)
1038 r << i;
1039 }
1040 return r;
1041 };
1042 for (int i = 3; i < count; ++i) {
1043 auto visible = connectableInHull(i);
1044 if (visible.isEmpty())
1045 continue;
1046 int visCount = visible.count();
1047 int hullCount = hull.count();
1048 // Find where the visible part of the hull starts. (This is the part we need to triangulate to,
1049 // and the part we're going to replace. "visible" contains the start point of the line segments that are visible from p.
1050 int boundaryStart = visible[0];
1051 for (int j = 0; j < visCount - 1; ++j) {
1052 if ((visible[j] + 1) % hullCount != visible[j+1]) {
1053 boundaryStart = visible[j + 1];
1054 break;
1055 }
1056 }
1057 // Finally replace the points that are now inside the hull
1058 // We insert the new point after boundaryStart, and before boundaryStart + visCount (modulo...)
1059 // and remove the points in between
1060 int pointsToKeep = hullCount - visCount + 1;
1061 QList<int> newHull;
1062 newHull << i;
1063 for (int j = 0; j < pointsToKeep; ++j) {
1064 newHull << hull.at(i: (j + boundaryStart + visCount) % hullCount);
1065 }
1066 hull = newHull;
1067 }
1068
1069 // Now that we have a convex hull, we can trivially triangulate it
1070 QList<TriangleData> ret;
1071 for (int i = 1; i < hull.size() - 1; ++i) {
1072 int i0 = hull[0];
1073 int i1 = hull[i];
1074 int i2 = hull[i+1];
1075 ret.append(t: {.points: {pts[i0], pts[i1], pts[i2]}, .pathElementIndex: elementIndex, .normals: {normals[i0], normals[i1], normals[i2]}});
1076 }
1077 return ret;
1078}
1079
1080static bool needsSplit(const QQuadPath::Element &el, float penWidth)
1081{
1082 Q_UNUSED(penWidth)
1083 const auto v1 = el.controlPoint() - el.startPoint();
1084 const auto v2 = el.endPoint() - el.controlPoint();
1085 float cos = QVector2D::dotProduct(v1, v2) / (v1.length() * v2.length());
1086 return cos < 0.9;
1087}
1088static void splitElementIfNecessary(QQuadPath &path, int index, float penWidth)
1089{
1090 auto &e = path.elementAt(i: index);
1091 if (e.isLine())
1092 return;
1093 if (e.childCount() == 0) {
1094 if (needsSplit(el: e, penWidth))
1095 path.splitElementAt(index);
1096 } else {
1097 for (int i = 0; i < e.childCount(); ++i)
1098 splitElementIfNecessary(path, index: e.indexOfChild(childNumber: i), penWidth);
1099 }
1100}
1101
1102static QQuadPath subdivide(const QQuadPath &path, int subdivisions, float penWidth)
1103{
1104 QQuadPath newPath = path;
1105
1106 for (int i = 0; i < subdivisions; ++i)
1107 for (int j = 0; j < newPath.elementCount(); j++)
1108 splitElementIfNecessary(path&: newPath, index: j, penWidth);
1109 return newPath;
1110}
1111
1112static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penWidth, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, float miterLimit)
1113{
1114 const bool bevelJoin = joinStyle == Qt::BevelJoin;
1115 const bool roundJoin = joinStyle == Qt::RoundJoin;
1116 const bool miterJoin = !bevelJoin && !roundJoin;
1117
1118 const bool roundCap = capStyle == Qt::RoundCap;
1119 const bool squareCap = capStyle == Qt::SquareCap;
1120 // We can't use the simple miter for miter joins, since the shader currently only supports round joins
1121 const bool simpleMiter = joinStyle == Qt::RoundJoin;
1122
1123 Q_ASSERT(miterLimit > 0 || !miterJoin);
1124 float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0;
1125
1126 const float penFactor = penWidth / 2;
1127
1128 // Returns {inner1, inner2, outer1, outer2, outerMiter}
1129 // where foo1 is for the end of element1 and foo2 is for the start of element2
1130 // and inner1 == inner2 unless we had to give up finding a decent point
1131 auto calculateJoin = [&](const QQuadPath::Element *element1, const QQuadPath::Element *element2,
1132 bool &outerBisectorWithinMiterLimit, bool &innerIsRight, bool &giveUp) -> std::array<QVector2D, 5>
1133 {
1134 outerBisectorWithinMiterLimit = true;
1135 innerIsRight = true;
1136 giveUp = false;
1137 if (!element1) {
1138 Q_ASSERT(element2);
1139 QVector2D n = normalVector(element: *element2).normalized();
1140 return {n, n, -n, -n, -n};
1141 }
1142 if (!element2) {
1143 Q_ASSERT(element1);
1144 QVector2D n = normalVector(element: *element1, endSide: true).normalized();
1145 return {n, n, -n, -n, -n};
1146 }
1147
1148 Q_ASSERT(element1->endPoint() == element2->startPoint());
1149
1150 const auto p1 = element1->isLine() ? element1->startPoint() : element1->controlPoint();
1151 const auto p2 = element1->endPoint();
1152 const auto p3 = element2->isLine() ? element2->endPoint() : element2->controlPoint();
1153
1154 const auto v1 = (p1 - p2).normalized();
1155 const auto v2 = (p3 - p2).normalized();
1156 const auto b = (v1 + v2);
1157
1158 constexpr float epsilon = 1.0f / 32.0f;
1159 bool smoothJoin = qAbs(t: b.x()) < epsilon && qAbs(t: b.y()) < epsilon;
1160
1161 if (smoothJoin) {
1162 // v1 and v2 are almost parallel and pointing in opposite directions
1163 // angle bisector formula will give an almost null vector: use normal of bisector of normals instead
1164 QVector2D n1(-v1.y(), v1.x());
1165 QVector2D n2(-v2.y(), v2.x());
1166 QVector2D n = (n2 - n1).normalized();
1167 return {n, n, -n, -n, -n};
1168 }
1169 // Calculate the length of the bisector, so it will cover the entire miter.
1170 // Using the identity sin(x/2) == sqrt((1 - cos(x)) / 2), and the fact that the
1171 // dot product of two unit vectors is the cosine of the angle between them
1172 // The length of the miter is w/sin(x/2) where x is the angle between the two elements
1173
1174 const auto bisector = b.normalized();
1175 float cos2x = QVector2D::dotProduct(v1, v2);
1176 cos2x = qMin(a: 1.0f, b: cos2x); // Allow for float inaccuracy
1177 float sine = sqrt(x: (1.0f - cos2x) / 2);
1178 innerIsRight = determinant(p1, p2, p3) > 0;
1179 sine = qMax(a: sine, b: 0.01f); // Avoid divide by zero
1180 float length = penFactor / sine;
1181
1182 // Check if bisector is longer than one of the lines it's trying to bisect
1183
1184 auto tooLong = [](QVector2D p1, QVector2D p2, QVector2D n, float length, float margin) -> bool {
1185 auto v = p2 - p1;
1186 // It's too long if the projection onto the bisector is longer than the bisector
1187 // and the projection onto the normal to the bisector is shorter
1188 // than the pen margin (that projection is just v - proj)
1189 // (we're adding a 10% safety margin to make room for AA -- not exact)
1190 auto projLen = QVector2D::dotProduct(v1: v, v2: n);
1191 return projLen * 0.9f < length && (v - n * projLen).length() * 0.9 < margin;
1192 };
1193
1194
1195 // The angle bisector of the tangent lines is not correct for curved lines. We could fix this by calculating
1196 // the exact intersection point, but for now just give up and use the normals.
1197
1198 giveUp = !element1->isLine() || !element2->isLine()
1199 || tooLong(p1, p2, bisector, length, penFactor)
1200 || tooLong(p3, p2, bisector, length, penFactor);
1201 outerBisectorWithinMiterLimit = sine >= inverseMiterLimit / 2.0f;
1202 bool simpleJoin = simpleMiter && outerBisectorWithinMiterLimit && !giveUp;
1203 const QVector2D bn = bisector / sine;
1204
1205 if (simpleJoin)
1206 return {bn, bn, -bn, -bn, -bn}; // We only have one inner and one outer point TODO: change inner point when conflict/curve
1207 const QVector2D n1 = normalVector(element: *element1, endSide: true).normalized();
1208 const QVector2D n2 = normalVector(element: *element2).normalized();
1209 if (giveUp) {
1210 if (innerIsRight)
1211 return {n1, n2, -n1, -n2, -bn};
1212 else
1213 return {-n1, -n2, n1, n2, -bn};
1214
1215 } else {
1216 if (innerIsRight)
1217 return {bn, bn, -n1, -n2, -bn};
1218 else
1219 return {bn, bn, n1, n2, -bn};
1220 }
1221 };
1222
1223 QList<TriangleData> ret;
1224
1225 auto triangulateCurve = [&](int idx, const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, const QVector2D &p4,
1226 const QVector2D &n1, const QVector2D &n2, const QVector2D &n3, const QVector2D &n4)
1227 {
1228 const auto &element = path.elementAt(i: idx);
1229 const auto &s = element.startPoint();
1230 const auto &c = element.controlPoint();
1231 const auto &e = element.endPoint();
1232 // TODO: Don't flatten the path in addCurveStrokeNodes, but iterate over the children here instead
1233 bool controlPointOnRight = determinant(p1: s, p2: c, p3: e) > 0;
1234 QVector2D startNormal = normalVector(element).normalized();
1235 QVector2D endNormal = normalVector(element, endSide: true).normalized();
1236 QVector2D controlPointNormal = (startNormal + endNormal).normalized();
1237 if (controlPointOnRight)
1238 controlPointNormal = -controlPointNormal;
1239 QVector2D p5 = c + controlPointNormal * penFactor; // This is too simplistic
1240 TrianglePoints t1{p1, p2, p5};
1241 TrianglePoints t2{p3, p4, p5};
1242 bool simpleCase = !checkTriangleOverlap(triangle1&: t1, triangle2&: t2);
1243
1244 if (simpleCase) {
1245 ret.append(t: {.points: {p1, p2, p5}, .pathElementIndex: idx, .normals: {n1, n2, controlPointNormal}});
1246 ret.append(t: {.points: {p3, p4, p5}, .pathElementIndex: idx, .normals: {n3, n4, controlPointNormal}});
1247 if (controlPointOnRight) {
1248 ret.append(t: {.points: {p1, p3, p5}, .pathElementIndex: idx, .normals: {n1, n3, controlPointNormal}});
1249 } else {
1250 ret.append(t: {.points: {p2, p4, p5}, .pathElementIndex: idx, .normals: {n2, n4, controlPointNormal}});
1251 }
1252 } else {
1253 ret.append(other: simplePointTriangulator(pts: {p1, p2, p5, p3, p4}, normals: {n1, n2, controlPointNormal, n3, n4}, elementIndex: idx));
1254 }
1255 };
1256
1257 // Each element is calculated independently, so we don't have to special-case closed sub-paths.
1258 // Take care so the end points of one element are precisely equal to the start points of the next.
1259 // Any additional triangles needed for joining are added at the end of the current element.
1260
1261 int count = path.elementCount();
1262 int subStart = 0;
1263 while (subStart < count) {
1264 int subEnd = subStart;
1265 for (int i = subStart + 1; i < count; ++i) {
1266 const auto &e = path.elementAt(i);
1267 if (e.isSubpathStart()) {
1268 subEnd = i - 1;
1269 break;
1270 }
1271 if (i == count - 1) {
1272 subEnd = i;
1273 break;
1274 }
1275 }
1276 bool closed = path.elementAt(i: subStart).startPoint() == path.elementAt(i: subEnd).endPoint();
1277 const int subCount = subEnd - subStart + 1;
1278
1279 auto addIdx = [&](int idx, int delta) -> int {
1280 int subIdx = idx - subStart;
1281 if (closed)
1282 subIdx = (subIdx + subCount + delta) % subCount;
1283 else
1284 subIdx += delta;
1285 return subStart + subIdx;
1286 };
1287 auto elementAt = [&](int idx, int delta) -> const QQuadPath::Element * {
1288 int subIdx = idx - subStart;
1289 if (closed) {
1290 subIdx = (subIdx + subCount + delta) % subCount;
1291 return &path.elementAt(i: subStart + subIdx);
1292 }
1293 subIdx += delta;
1294 if (subIdx >= 0 && subIdx < subCount)
1295 return &path.elementAt(i: subStart + subIdx);
1296 return nullptr;
1297 };
1298
1299 for (int i = subStart; i <= subEnd; ++i) {
1300 const auto &element = path.elementAt(i);
1301 const auto *nextElement = elementAt(i, +1);
1302 const auto *prevElement = elementAt(i, -1);
1303
1304 const auto &s = element.startPoint();
1305 const auto &e = element.endPoint();
1306
1307 bool startInnerIsRight;
1308 bool startBisectorWithinMiterLimit; // Not used
1309 bool giveUpOnStartJoin; // Not used
1310 auto startJoin = calculateJoin(prevElement, &element,
1311 startBisectorWithinMiterLimit, startInnerIsRight,
1312 giveUpOnStartJoin);
1313 const QVector2D &startInner = startJoin[1];
1314 const QVector2D &startOuter = startJoin[3];
1315
1316 bool endInnerIsRight;
1317 bool endBisectorWithinMiterLimit;
1318 bool giveUpOnEndJoin;
1319 auto endJoin = calculateJoin(&element, nextElement,
1320 endBisectorWithinMiterLimit, endInnerIsRight,
1321 giveUpOnEndJoin);
1322 QVector2D endInner = endJoin[0];
1323 QVector2D endOuter = endJoin[2];
1324 QVector2D nextOuter = endJoin[3];
1325 QVector2D outerB = endJoin[4];
1326
1327 QVector2D p1, p2, p3, p4;
1328 QVector2D n1, n2, n3, n4;
1329
1330 if (startInnerIsRight) {
1331 n1 = startInner;
1332 n2 = startOuter;
1333 } else {
1334 n1 = startOuter;
1335 n2 = startInner;
1336 }
1337
1338 p1 = s + n1 * penFactor;
1339 p2 = s + n2 * penFactor;
1340
1341 // repeat logic above for the other end:
1342 if (endInnerIsRight) {
1343 n3 = endInner;
1344 n4 = endOuter;
1345 } else {
1346 n3 = endOuter;
1347 n4 = endInner;
1348 }
1349
1350 p3 = e + n3 * penFactor;
1351 p4 = e + n4 * penFactor;
1352
1353 // End caps
1354
1355 if (!prevElement) {
1356 QVector2D capSpace = tangentVector(element).normalized() * -penFactor;
1357 if (roundCap) {
1358 p1 += capSpace;
1359 p2 += capSpace;
1360 } else if (squareCap) {
1361 QVector2D c1 = p1 + capSpace;
1362 QVector2D c2 = p2 + capSpace;
1363 ret.append(t: {.points: {p1, s, c1}, .pathElementIndex: -1, .normals: {}});
1364 ret.append(t: {.points: {c1, s, c2}, .pathElementIndex: -1, .normals: {}});
1365 ret.append(t: {.points: {p2, s, c2}, .pathElementIndex: -1, .normals: {}});
1366 }
1367 }
1368 if (!nextElement) {
1369 QVector2D capSpace = tangentVector(element, endSide: true).normalized() * -penFactor;
1370 if (roundCap) {
1371 p3 += capSpace;
1372 p4 += capSpace;
1373 } else if (squareCap) {
1374 QVector2D c3 = p3 + capSpace;
1375 QVector2D c4 = p4 + capSpace;
1376 ret.append(t: {.points: {p3, e, c3}, .pathElementIndex: -1, .normals: {}});
1377 ret.append(t: {.points: {c3, e, c4}, .pathElementIndex: -1, .normals: {}});
1378 ret.append(t: {.points: {p4, e, c4}, .pathElementIndex: -1, .normals: {}});
1379 }
1380 }
1381
1382 if (element.isLine()) {
1383 ret.append(t: {.points: {p1, p2, p3}, .pathElementIndex: i, .normals: {n1, n2, n3}});
1384 ret.append(t: {.points: {p2, p3, p4}, .pathElementIndex: i, .normals: {n2, n3, n4}});
1385 } else {
1386 triangulateCurve(i, p1, p2, p3, p4, n1, n2, n3, n4);
1387 }
1388
1389 bool trivialJoin = simpleMiter && endBisectorWithinMiterLimit && !giveUpOnEndJoin;
1390 if (!trivialJoin && nextElement) {
1391 // inside of join (opposite of bevel) is defined by
1392 // triangle s, e, next.e
1393 bool innerOnRight = endInnerIsRight;
1394
1395 const auto outer1 = e + endOuter * penFactor;
1396 const auto outer2 = e + nextOuter * penFactor;
1397 //const auto inner = e + endInner * penFactor;
1398
1399 if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) {
1400 ret.append(t: {.points: {outer1, e, outer2}, .pathElementIndex: -1, .normals: {}});
1401 } else if (roundJoin) {
1402 ret.append(t: {.points: {outer1, e, outer2}, .pathElementIndex: i, .normals: {}});
1403 QVector2D nn = calcNormalVector(a: outer1, b: outer2).normalized() * penFactor;
1404 if (!innerOnRight)
1405 nn = -nn;
1406 ret.append(t: {.points: {outer1, outer1 + nn, outer2}, .pathElementIndex: i, .normals: {}});
1407 ret.append(t: {.points: {outer1 + nn, outer2, outer2 + nn}, .pathElementIndex: i, .normals: {}});
1408
1409 } else if (miterJoin) {
1410 QVector2D outer = e + outerB * penFactor;
1411 ret.append(t: {.points: {outer1, e, outer}, .pathElementIndex: -2, .normals: {}});
1412 ret.append(t: {.points: {outer, e, outer2}, .pathElementIndex: -2, .normals: {}});
1413 }
1414
1415 if (!giveUpOnEndJoin) {
1416 QVector2D inner = e + endInner * penFactor;
1417 ret.append(t: {.points: {inner, e, outer1}, .pathElementIndex: i, .normals: {endInner, {}, endOuter}});
1418 // The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet)
1419 int nextIdx = addIdx(i, +1);
1420 ret.append(t: {.points: {inner, e, outer2}, .pathElementIndex: nextIdx, .normals: {endInner, {}, nextOuter}});
1421 }
1422 }
1423 }
1424 subStart = subEnd + 1;
1425 }
1426 return ret;
1427}
1428
1429};
1430
1431QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addCurveStrokeNodes(const PathData &pathData, NodeList *debugNodes)
1432{
1433 QVector<QSGGeometryNode *> ret;
1434 const QColor &color = pathData.pen.color();
1435
1436 const bool debug = debugVisualization() & DebugCurves;
1437 auto *node = new QQuickShapeStrokeNode;
1438 node->setDebug(0.2f * debug);
1439 QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
1440
1441 float miterLimit = pathData.pen.miterLimit();
1442 float penWidth = pathData.pen.widthF();
1443
1444 static const int subdivisions = qEnvironmentVariable(varName: "QT_QUICKSHAPES_STROKE_SUBDIVISIONS", QStringLiteral("3")).toInt();
1445 auto thePath = subdivide(path: pathData.strokePath, subdivisions, penWidth).flattened(); // TODO: don't flatten, but handle it in the triangulator
1446 auto triangles = customTriangulator2(path: thePath, penWidth, joinStyle: pathData.pen.joinStyle(), capStyle: pathData.pen.capStyle(), miterLimit);
1447
1448 auto addCurveTriangle = [&](const QQuadPath::Element &element, const TriangleData &t){
1449 const QVector2D &p0 = t.points[0];
1450 const QVector2D &p1 = t.points[1];
1451 const QVector2D &p2 = t.points[2];
1452 if (element.isLine()) {
1453 node->appendTriangle(v: t.points,
1454 p: LinePoints{element.startPoint(), element.endPoint()},
1455 n: t.normals);
1456 } else {
1457 node->appendTriangle(v: t.points,
1458 p: { element.startPoint(), element.controlPoint(), element.endPoint()},
1459 n: t.normals);
1460 }
1461 wfVertices.append(t: {.x: p0.x(), .y: p0.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f});
1462 wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 0.0f, .v: 1.0f, .w: 0.0f});
1463 wfVertices.append(t: {.x: p2.x(), .y: p2.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f});
1464 };
1465
1466 auto addBevelTriangle = [&](const TrianglePoints &p)
1467 {
1468 QVector2D fp1 = p[0];
1469 QVector2D fp2 = p[2];
1470
1471 // That describes a path that passes through those points. We want the stroke
1472 // edge, so we need to shift everything down by the stroke offset
1473
1474 QVector2D nn = calcNormalVector(a: p[0], b: p[2]);
1475 if (determinant(p) < 0)
1476 nn = -nn;
1477 float delta = penWidth / 2;
1478
1479 QVector2D offset = nn.normalized() * delta;
1480 fp1 += offset;
1481 fp2 += offset;
1482
1483 TrianglePoints n;
1484 // p1 is inside, so n[1] is {0,0}
1485 n[0] = (p[0] - p[1]).normalized();
1486 n[2] = (p[2] - p[1]).normalized();
1487
1488 node->appendTriangle(v: p, p: LinePoints{fp1, fp2}, n);
1489
1490 wfVertices.append(t: {.x: p[0].x(), .y: p[0].y(), .u: 1.0f, .v: 0.0f, .w: 0.0f});
1491 wfVertices.append(t: {.x: p[1].x(), .y: p[1].y(), .u: 0.0f, .v: 1.0f, .w: 0.0f});
1492 wfVertices.append(t: {.x: p[2].x(), .y: p[2].y(), .u: 0.0f, .v: 0.0f, .w: 1.0f});
1493 };
1494
1495 for (const auto &triangle : triangles) {
1496 if (triangle.pathElementIndex < 0) {
1497 addBevelTriangle(triangle.points);
1498 continue;
1499 }
1500 const auto &element = thePath.elementAt(i: triangle.pathElementIndex);
1501 addCurveTriangle(element, triangle);
1502 }
1503
1504 auto indexCopy = node->uncookedIndexes(); // uncookedIndexes get delete on cooking
1505
1506 node->setColor(color);
1507 node->setStrokeWidth(pathData.pen.widthF());
1508 node->cookGeometry();
1509 m_rootNode->appendChildNode(node);
1510 ret.append(t: node);
1511
1512 const bool wireFrame = debugVisualization() & DebugWireframe;
1513 if (wireFrame) {
1514 QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
1515
1516 QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
1517 wfVertices.size(),
1518 indexCopy.size(),
1519 QSGGeometry::UnsignedIntType);
1520 wfNode->setGeometry(wfg);
1521
1522 wfg->setDrawingMode(QSGGeometry::DrawTriangles);
1523 memcpy(dest: wfg->indexData(),
1524 src: indexCopy.data(),
1525 n: indexCopy.size() * wfg->sizeOfIndex());
1526 memcpy(dest: wfg->vertexData(),
1527 src: wfVertices.data(),
1528 n: wfg->vertexCount() * wfg->sizeOfVertex());
1529
1530 m_rootNode->appendChildNode(node: wfNode);
1531 debugNodes->append(t: wfNode);
1532 }
1533
1534 return ret;
1535}
1536
1537// TODO: we could optimize by preprocessing e1, since we call this function multiple times on the same
1538// elements
1539static void handleOverlap(QQuadPath &path, int e1, int e2, int recursionLevel = 0)
1540{
1541 if (!isOverlap(path, e1, e2)) {
1542 return;
1543 }
1544
1545 if (recursionLevel > 8) {
1546 qCDebug(lcShapeCurveRenderer) << "Triangle overlap: recursion level" << recursionLevel << "aborting!";
1547 return;
1548 }
1549
1550 if (path.elementAt(i: e1).childCount() > 1) {
1551 auto e11 = path.indexOfChildAt(i: e1, childNumber: 0);
1552 auto e12 = path.indexOfChildAt(i: e1, childNumber: 1);
1553 handleOverlap(path, e1: e11, e2, recursionLevel: recursionLevel + 1);
1554 handleOverlap(path, e1: e12, e2, recursionLevel: recursionLevel + 1);
1555 } else if (path.elementAt(i: e2).childCount() > 1) {
1556 auto e21 = path.indexOfChildAt(i: e2, childNumber: 0);
1557 auto e22 = path.indexOfChildAt(i: e2, childNumber: 1);
1558 handleOverlap(path, e1, e2: e21, recursionLevel: recursionLevel + 1);
1559 handleOverlap(path, e1, e2: e22, recursionLevel: recursionLevel + 1);
1560 } else {
1561 path.splitElementAt(index: e1);
1562 auto e11 = path.indexOfChildAt(i: e1, childNumber: 0);
1563 auto e12 = path.indexOfChildAt(i: e1, childNumber: 1);
1564 bool overlap1 = isOverlap(path, e1: e11, e2);
1565 bool overlap2 = isOverlap(path, e1: e12, e2);
1566 if (!overlap1 && !overlap2)
1567 return; // no more overlap: success!
1568
1569 // We need to split more:
1570 if (path.elementAt(i: e2).isLine()) {
1571 // Splitting a line won't help, so we just split e1 further
1572 if (overlap1)
1573 handleOverlap(path, e1: e11, e2, recursionLevel: recursionLevel + 1);
1574 if (overlap2)
1575 handleOverlap(path, e1: e12, e2, recursionLevel: recursionLevel + 1);
1576 } else {
1577 // See if splitting e2 works:
1578 path.splitElementAt(index: e2);
1579 auto e21 = path.indexOfChildAt(i: e2, childNumber: 0);
1580 auto e22 = path.indexOfChildAt(i: e2, childNumber: 1);
1581 if (overlap1) {
1582 handleOverlap(path, e1: e11, e2: e21, recursionLevel: recursionLevel + 1);
1583 handleOverlap(path, e1: e11, e2: e22, recursionLevel: recursionLevel + 1);
1584 }
1585 if (overlap2) {
1586 handleOverlap(path, e1: e12, e2: e21, recursionLevel: recursionLevel + 1);
1587 handleOverlap(path, e1: e12, e2: e22, recursionLevel: recursionLevel + 1);
1588 }
1589 }
1590 }
1591}
1592
1593// Test if element contains a start point of another element
1594static void handleOverlap(QQuadPath &path, int e1, const QVector2D vertex, int recursionLevel = 0)
1595{
1596 // First of all: Ignore the next element: it trivially overlaps (maybe not necessary: we do check for strict containment)
1597 if (vertex == path.elementAt(i: e1).endPoint() || !isOverlap(path, index: e1, vertex))
1598 return;
1599 if (recursionLevel > 8) {
1600 qCDebug(lcShapeCurveRenderer) << "Vertex overlap: recursion level" << recursionLevel << "aborting!";
1601 return;
1602 }
1603
1604 // Don't split if we're already split
1605 if (path.elementAt(i: e1).childCount() == 0)
1606 path.splitElementAt(index: e1);
1607
1608 handleOverlap(path, e1: path.indexOfChildAt(i: e1, childNumber: 0), vertex, recursionLevel: recursionLevel + 1);
1609 handleOverlap(path, e1: path.indexOfChildAt(i: e1, childNumber: 1), vertex, recursionLevel: recursionLevel + 1);
1610}
1611
1612void QQuickShapeCurveRenderer::solveOverlaps(QQuadPath &path)
1613{
1614 for (int i = 0; i < path.elementCount(); i++) {
1615 auto &element = path.elementAt(i);
1616 // only concave curve overlap is problematic, as long as we don't allow self-intersecting curves
1617 if (element.isLine() || element.isConvex())
1618 continue;
1619
1620 for (int j = 0; j < path.elementCount(); j++) {
1621 if (i == j)
1622 continue; // Would be silly to test overlap with self
1623 auto &other = path.elementAt(i: j);
1624 if (!other.isConvex() && !other.isLine() && j < i)
1625 continue; // We have already tested this combination, so no need to test again
1626 handleOverlap(path, e1: i, e2: j);
1627 }
1628 }
1629
1630 static const int handleConcaveJoint = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_WIP_CONCAVE_JOINT");
1631 if (handleConcaveJoint) {
1632 // Note that the joint between two non-concave elements can also be concave, so we have to
1633 // test all convex elements to see if there is a vertex in any of them. We could do it the other way
1634 // by identifying concave joints, but then we would have to know which side is the inside
1635 // TODO: optimization potential! Maybe do that at the same time as we identify concave curves?
1636
1637 // We do this in a separate loop, since the triangle/triangle test above is more expensive, and
1638 // if we did this first, there would be more triangles to test
1639 for (int i = 0; i < path.elementCount(); i++) {
1640 auto &element = path.elementAt(i);
1641 if (!element.isConvex())
1642 continue;
1643
1644 for (int j = 0; j < path.elementCount(); j++) {
1645 // We only need to check one point per element, since all subpaths are closed
1646 // Could do smartness to skip elements that cannot overlap, but let's do it the easy way first
1647 if (i == j)
1648 continue;
1649 const auto &other = path.elementAt(i: j);
1650 handleOverlap(path, e1: i, vertex: other.startPoint());
1651 }
1652 }
1653 }
1654}
1655
1656QT_END_NAMESPACE
1657

source code of qtdeclarative/src/quickshapes/qquickshapecurverenderer.cpp