1 | // Copyright (C) 2024 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 | |
7 | #if QT_CONFIG(thread) |
8 | #include <QtCore/qthreadpool.h> |
9 | #endif |
10 | |
11 | #include <QtGui/qvector2d.h> |
12 | #include <QtGui/qvector4d.h> |
13 | #include <QtGui/private/qtriangulator_p.h> |
14 | #include <QtGui/private/qtriangulatingstroker_p.h> |
15 | #include <QtGui/private/qrhi_p.h> |
16 | |
17 | #include <QtQuick/private/qsgcurvefillnode_p.h> |
18 | #include <QtQuick/private/qsgcurvestrokenode_p.h> |
19 | #include <QtQuick/private/qquadpath_p.h> |
20 | #include <QtQuick/private/qsgcurveprocessor_p.h> |
21 | #include <QtQuick/qsgmaterial.h> |
22 | |
23 | QT_BEGIN_NAMESPACE |
24 | |
25 | Q_LOGGING_CATEGORY(lcShapeCurveRenderer, "qt.shape.curverenderer"); |
26 | |
27 | namespace { |
28 | |
29 | class QQuickShapeWireFrameMaterialShader : public QSGMaterialShader |
30 | { |
31 | public: |
32 | QQuickShapeWireFrameMaterialShader(int viewCount) |
33 | { |
34 | setShaderFileName(stage: VertexStage, |
35 | QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.vert.qsb"), viewCount); |
36 | setShaderFileName(stage: FragmentStage, |
37 | QStringLiteral(":/qt-project.org/shapes/shaders_ng/wireframe.frag.qsb"), viewCount); |
38 | } |
39 | |
40 | bool updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *) override |
41 | { |
42 | bool changed = false; |
43 | QByteArray *buf = state.uniformData(); |
44 | Q_ASSERT(buf->size() >= 64); |
45 | const int matrixCount = qMin(a: state.projectionMatrixCount(), b: newMaterial->viewCount()); |
46 | |
47 | for (int viewIndex = 0; viewIndex < matrixCount; ++viewIndex) { |
48 | if (state.isMatrixDirty()) { |
49 | const QMatrix4x4 m = state.combinedMatrix(index: viewIndex); |
50 | memcpy(dest: buf->data() + 64 * viewIndex, src: m.constData(), n: 64); |
51 | changed = true; |
52 | } |
53 | } |
54 | |
55 | return changed; |
56 | } |
57 | }; |
58 | |
59 | class QQuickShapeWireFrameMaterial : public QSGMaterial |
60 | { |
61 | public: |
62 | QQuickShapeWireFrameMaterial() |
63 | { |
64 | setFlag(flags: Blending, on: true); |
65 | } |
66 | |
67 | int compare(const QSGMaterial *other) const override |
68 | { |
69 | return (type() - other->type()); |
70 | } |
71 | |
72 | protected: |
73 | QSGMaterialType *type() const override |
74 | { |
75 | static QSGMaterialType t; |
76 | return &t; |
77 | } |
78 | QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override |
79 | { |
80 | return new QQuickShapeWireFrameMaterialShader(viewCount()); |
81 | } |
82 | |
83 | }; |
84 | |
85 | class QQuickShapeWireFrameNode : public QSGCurveAbstractNode |
86 | { |
87 | public: |
88 | struct WireFrameVertex |
89 | { |
90 | float x, y, u, v, w; |
91 | }; |
92 | |
93 | QQuickShapeWireFrameNode() |
94 | { |
95 | isDebugNode = true; |
96 | setFlag(OwnsGeometry, true); |
97 | setGeometry(new QSGGeometry(attributes(), 0, 0)); |
98 | activateMaterial(); |
99 | } |
100 | |
101 | void setColor(QColor col) override |
102 | { |
103 | Q_UNUSED(col); |
104 | } |
105 | |
106 | void activateMaterial() |
107 | { |
108 | m_material.reset(other: new QQuickShapeWireFrameMaterial); |
109 | setMaterial(m_material.data()); |
110 | } |
111 | |
112 | static const QSGGeometry::AttributeSet &attributes() |
113 | { |
114 | static QSGGeometry::Attribute data[] = { |
115 | QSGGeometry::Attribute::createWithAttributeType(pos: 0, tupleSize: 2, primitiveType: QSGGeometry::FloatType, attributeType: QSGGeometry::PositionAttribute), |
116 | QSGGeometry::Attribute::createWithAttributeType(pos: 1, tupleSize: 3, primitiveType: QSGGeometry::FloatType, attributeType: QSGGeometry::TexCoordAttribute), |
117 | }; |
118 | static QSGGeometry::AttributeSet attrs = { .count: 2, .stride: sizeof(WireFrameVertex), .attributes: data }; |
119 | return attrs; |
120 | } |
121 | |
122 | void cookGeometry() override |
123 | { |
124 | // Intentionally empty |
125 | } |
126 | |
127 | protected: |
128 | QScopedPointer<QQuickShapeWireFrameMaterial> m_material; |
129 | }; |
130 | } |
131 | |
132 | QQuickShapeCurveRenderer::~QQuickShapeCurveRenderer() |
133 | { |
134 | for (const PathData &pd : std::as_const(t&: m_paths)) { |
135 | if (pd.currentRunner) { |
136 | pd.currentRunner->orphaned = true; |
137 | if (!pd.currentRunner->isAsync || pd.currentRunner->isDone) |
138 | delete pd.currentRunner; |
139 | } |
140 | } |
141 | } |
142 | |
143 | void QQuickShapeCurveRenderer::beginSync(int totalCount, bool *countChanged) |
144 | { |
145 | if (countChanged != nullptr && totalCount != m_paths.size()) |
146 | *countChanged = true; |
147 | for (int i = totalCount; i < m_paths.size(); i++) { // Handle removal of paths |
148 | setFillTextureProvider(index: i, textureProviderItem: nullptr); // deref window |
149 | m_removedPaths.append(t: m_paths.at(i)); |
150 | } |
151 | m_paths.resize(size: totalCount); |
152 | } |
153 | |
154 | void QQuickShapeCurveRenderer::setPath(int index, const QQuickPath *path) |
155 | { |
156 | constexpr QQuickShapePath::PathHints noHints; |
157 | const auto *shapePath = qobject_cast<const QQuickShapePath *>(object: path); |
158 | setPath(index, path: path->path(), pathHints: shapePath ? shapePath->pathHints() : noHints); |
159 | } |
160 | |
161 | void QQuickShapeCurveRenderer::setPath(int index, const QPainterPath &path, QQuickShapePath::PathHints pathHints) |
162 | { |
163 | auto &pathData = m_paths[index]; |
164 | pathData.originalPath = path; |
165 | pathData.pathHints = pathHints; |
166 | pathData.m_dirty |= PathDirty; |
167 | } |
168 | |
169 | void QQuickShapeCurveRenderer::setStrokeColor(int index, const QColor &color) |
170 | { |
171 | auto &pathData = m_paths[index]; |
172 | const bool wasVisible = pathData.isStrokeVisible(); |
173 | pathData.pen.setColor(color); |
174 | if (pathData.isStrokeVisible() != wasVisible) |
175 | pathData.m_dirty |= StrokeDirty; |
176 | else |
177 | pathData.m_dirty |= UniformsDirty; |
178 | } |
179 | |
180 | void QQuickShapeCurveRenderer::setStrokeWidth(int index, qreal w) |
181 | { |
182 | auto &pathData = m_paths[index]; |
183 | if (w > 0) { |
184 | pathData.validPenWidth = true; |
185 | pathData.pen.setWidthF(w); |
186 | } else { |
187 | pathData.validPenWidth = false; |
188 | } |
189 | pathData.m_dirty |= StrokeDirty; |
190 | } |
191 | |
192 | void QQuickShapeCurveRenderer::setFillColor(int index, const QColor &color) |
193 | { |
194 | auto &pathData = m_paths[index]; |
195 | const bool wasVisible = pathData.isFillVisible(); |
196 | pathData.fillColor = color; |
197 | if (pathData.isFillVisible() != wasVisible) |
198 | pathData.m_dirty |= FillDirty; |
199 | else |
200 | pathData.m_dirty |= UniformsDirty; |
201 | } |
202 | |
203 | void QQuickShapeCurveRenderer::setFillRule(int index, QQuickShapePath::FillRule fillRule) |
204 | { |
205 | auto &pathData = m_paths[index]; |
206 | pathData.fillRule = Qt::FillRule(fillRule); |
207 | pathData.m_dirty |= PathDirty; |
208 | } |
209 | |
210 | void QQuickShapeCurveRenderer::setJoinStyle(int index, |
211 | QQuickShapePath::JoinStyle joinStyle, |
212 | int miterLimit) |
213 | { |
214 | auto &pathData = m_paths[index]; |
215 | pathData.pen.setJoinStyle(Qt::PenJoinStyle(joinStyle)); |
216 | pathData.pen.setMiterLimit(miterLimit); |
217 | pathData.m_dirty |= StrokeDirty; |
218 | } |
219 | |
220 | void QQuickShapeCurveRenderer::setCapStyle(int index, QQuickShapePath::CapStyle capStyle) |
221 | { |
222 | auto &pathData = m_paths[index]; |
223 | pathData.pen.setCapStyle(Qt::PenCapStyle(capStyle)); |
224 | pathData.m_dirty |= StrokeDirty; |
225 | } |
226 | |
227 | void QQuickShapeCurveRenderer::setStrokeStyle(int index, |
228 | QQuickShapePath::StrokeStyle strokeStyle, |
229 | qreal dashOffset, |
230 | const QVector<qreal> &dashPattern) |
231 | { |
232 | auto &pathData = m_paths[index]; |
233 | pathData.pen.setStyle(Qt::PenStyle(strokeStyle)); |
234 | if (strokeStyle == QQuickShapePath::DashLine) { |
235 | pathData.pen.setDashPattern(dashPattern); |
236 | pathData.pen.setDashOffset(dashOffset); |
237 | } |
238 | pathData.m_dirty |= StrokeDirty; |
239 | } |
240 | |
241 | void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *gradient) |
242 | { |
243 | PathData &pd(m_paths[index]); |
244 | const bool wasVisible = pd.isFillVisible(); |
245 | pd.gradientType = QGradient::NoGradient; |
246 | if (QQuickShapeLinearGradient *g = qobject_cast<QQuickShapeLinearGradient *>(object: gradient)) { |
247 | pd.gradientType = QGradient::LinearGradient; |
248 | pd.gradient.stops = gradient->gradientStops(); |
249 | pd.gradient.spread = QGradient::Spread(gradient->spread()); |
250 | pd.gradient.a = QPointF(g->x1(), g->y1()); |
251 | pd.gradient.b = QPointF(g->x2(), g->y2()); |
252 | } else if (QQuickShapeRadialGradient *g = qobject_cast<QQuickShapeRadialGradient *>(object: gradient)) { |
253 | pd.gradientType = QGradient::RadialGradient; |
254 | pd.gradient.a = QPointF(g->centerX(), g->centerY()); |
255 | pd.gradient.b = QPointF(g->focalX(), g->focalY()); |
256 | pd.gradient.v0 = g->centerRadius(); |
257 | pd.gradient.v1 = g->focalRadius(); |
258 | } else if (QQuickShapeConicalGradient *g = qobject_cast<QQuickShapeConicalGradient *>(object: gradient)) { |
259 | pd.gradientType = QGradient::ConicalGradient; |
260 | pd.gradient.a = QPointF(g->centerX(), g->centerY()); |
261 | pd.gradient.v0 = g->angle(); |
262 | } else if (gradient != nullptr) { |
263 | static bool warned = false; |
264 | if (!warned) { |
265 | warned = true; |
266 | qCWarning(lcShapeCurveRenderer) << "Unsupported gradient fill"; |
267 | } |
268 | } |
269 | |
270 | if (pd.gradientType != QGradient::NoGradient) { |
271 | pd.gradient.stops = gradient->gradientStops(); |
272 | pd.gradient.spread = QGradient::Spread(gradient->spread()); |
273 | } |
274 | |
275 | pd.m_dirty |= (pd.isFillVisible() != wasVisible) ? FillDirty : UniformsDirty; |
276 | } |
277 | |
278 | void QQuickShapeCurveRenderer::setFillTransform(int index, const QSGTransform &transform) |
279 | { |
280 | auto &pathData = m_paths[index]; |
281 | pathData.fillTransform = transform; |
282 | pathData.m_dirty |= UniformsDirty; |
283 | } |
284 | |
285 | void QQuickShapeCurveRenderer::setFillTextureProvider(int index, QQuickItem *textureProviderItem) |
286 | { |
287 | auto &pathData = m_paths[index]; |
288 | const bool wasVisible = pathData.isFillVisible(); |
289 | if (pathData.fillTextureProviderItem != nullptr) |
290 | QQuickItemPrivate::get(item: pathData.fillTextureProviderItem)->derefWindow(); |
291 | pathData.fillTextureProviderItem = textureProviderItem; |
292 | if (pathData.fillTextureProviderItem != nullptr) |
293 | QQuickItemPrivate::get(item: pathData.fillTextureProviderItem)->refWindow(m_item->window()); |
294 | pathData.m_dirty |= (pathData.isFillVisible() != wasVisible) ? FillDirty : UniformsDirty; |
295 | } |
296 | |
297 | void QQuickShapeCurveRenderer::handleSceneChange(QQuickWindow *window) |
298 | { |
299 | for (auto &pathData : m_paths) { |
300 | if (pathData.fillTextureProviderItem != nullptr) { |
301 | if (window == nullptr) |
302 | QQuickItemPrivate::get(item: pathData.fillTextureProviderItem)->derefWindow(); |
303 | else |
304 | QQuickItemPrivate::get(item: pathData.fillTextureProviderItem)->refWindow(window); |
305 | } |
306 | } |
307 | } |
308 | |
309 | void QQuickShapeCurveRenderer::setAsyncCallback(void (*callback)(void *), void *data) |
310 | { |
311 | m_asyncCallback = callback; |
312 | m_asyncCallbackData = data; |
313 | } |
314 | |
315 | void QQuickShapeCurveRenderer::endSync(bool async) |
316 | { |
317 | bool asyncThreadsRunning = false; |
318 | |
319 | for (PathData &pathData : m_paths) { |
320 | if (!pathData.m_dirty) |
321 | continue; |
322 | |
323 | if (pathData.m_dirty == UniformsDirty) { |
324 | // Requires no curve node computation, gets handled directly in updateNode() |
325 | continue; |
326 | } |
327 | |
328 | if (pathData.currentRunner) { |
329 | // Already performing async computing. New dirty flags will be handled in the next sync |
330 | // after the current computation is done and the item is updated |
331 | asyncThreadsRunning = true; |
332 | continue; |
333 | } |
334 | |
335 | pathData.currentRunner = new QQuickShapeCurveRunnable; |
336 | setUpRunner(&pathData); |
337 | |
338 | #if QT_CONFIG(thread) |
339 | if (async) { |
340 | pathData.currentRunner->isAsync = true; |
341 | QThreadPool::globalInstance()->start(runnable: pathData.currentRunner); |
342 | asyncThreadsRunning = true; |
343 | } else |
344 | #endif |
345 | { |
346 | pathData.currentRunner->run(); |
347 | } |
348 | } |
349 | |
350 | if (async && !asyncThreadsRunning && m_asyncCallback) |
351 | m_asyncCallback(m_asyncCallbackData); |
352 | } |
353 | |
354 | void QQuickShapeCurveRenderer::setUpRunner(PathData *pathData) |
355 | { |
356 | Q_ASSERT(pathData->currentRunner); |
357 | QQuickShapeCurveRunnable *runner = pathData->currentRunner; |
358 | runner->isDone = false; |
359 | runner->pathData = *pathData; |
360 | runner->pathData.fillNodes.clear(); |
361 | runner->pathData.strokeNodes.clear(); |
362 | runner->pathData.currentRunner = nullptr; |
363 | pathData->m_dirty = 0; |
364 | if (!runner->isInitialized) { |
365 | runner->isInitialized = true; |
366 | runner->setAutoDelete(false); |
367 | QObject::connect(sender: runner, signal: &QQuickShapeCurveRunnable::done, qApp, |
368 | slot: [this](QQuickShapeCurveRunnable *r) { |
369 | r->isDone = true; |
370 | if (r->orphaned) { |
371 | delete r; // Renderer was destroyed |
372 | } else if (r->isAsync) { |
373 | maybeUpdateAsyncItem(); |
374 | } |
375 | }); |
376 | } |
377 | } |
378 | |
379 | void QQuickShapeCurveRenderer::maybeUpdateAsyncItem() |
380 | { |
381 | for (const PathData &pd : std::as_const(t&: m_paths)) { |
382 | if (pd.currentRunner && !pd.currentRunner->isDone) |
383 | return; |
384 | } |
385 | if (m_item) |
386 | m_item->update(); |
387 | if (m_asyncCallback) |
388 | m_asyncCallback(m_asyncCallbackData); |
389 | } |
390 | |
391 | QQuickShapeCurveRunnable::~QQuickShapeCurveRunnable() |
392 | { |
393 | qDeleteAll(c: pathData.fillNodes); |
394 | qDeleteAll(c: pathData.strokeNodes); |
395 | } |
396 | |
397 | void QQuickShapeCurveRunnable::run() |
398 | { |
399 | QQuickShapeCurveRenderer::processPath(pathData: &pathData); |
400 | emit done(self: this); |
401 | } |
402 | |
403 | void QQuickShapeCurveRenderer::updateNode() |
404 | { |
405 | if (!m_rootNode) |
406 | return; |
407 | |
408 | auto updateUniforms = [](const PathData &pathData) { |
409 | for (auto &pathNode : std::as_const(t: pathData.fillNodes)) { |
410 | if (pathNode->isDebugNode) |
411 | continue; |
412 | QSGCurveFillNode *fillNode = static_cast<QSGCurveFillNode *>(pathNode); |
413 | fillNode->setColor(pathData.fillColor); |
414 | fillNode->setGradientType(pathData.gradientType); |
415 | fillNode->setFillGradient(pathData.gradient); |
416 | fillNode->setFillTransform(pathData.fillTransform); |
417 | fillNode->setFillTextureProvider(pathData.fillTextureProviderItem != nullptr |
418 | ? pathData.fillTextureProviderItem->textureProvider() |
419 | : nullptr); |
420 | } |
421 | for (auto &strokeNode : std::as_const(t: pathData.strokeNodes)) |
422 | strokeNode->setColor(pathData.pen.color()); |
423 | }; |
424 | |
425 | NodeList toBeDeleted; |
426 | |
427 | for (const PathData &pathData : std::as_const(t&: m_removedPaths)) { |
428 | toBeDeleted += pathData.fillNodes; |
429 | toBeDeleted += pathData.strokeNodes; |
430 | } |
431 | m_removedPaths.clear(); |
432 | |
433 | for (int i = 0; i < m_paths.size(); i++) { |
434 | PathData &pathData = m_paths[i]; |
435 | if (pathData.currentRunner) { |
436 | if (!pathData.currentRunner->isDone) |
437 | continue; |
438 | // Find insertion point for new nodes. Default is the first stroke node of this path |
439 | QSGNode *nextNode = pathData.strokeNodes.value(i: 0); |
440 | // If that is 0, use the first node (stroke or fill) of later paths, if any |
441 | for (int j = i + 1; !nextNode && j < m_paths.size(); j++) { |
442 | const PathData &pd = m_paths[j]; |
443 | nextNode = pd.fillNodes.isEmpty() ? pd.strokeNodes.value(i: 0) : pd.fillNodes.value(i: 0); |
444 | } |
445 | |
446 | PathData &newData = pathData.currentRunner->pathData; |
447 | if (newData.m_dirty & PathDirty) |
448 | pathData.path = newData.path; |
449 | if (newData.m_dirty & FillDirty) { |
450 | pathData.fillPath = newData.fillPath; |
451 | for (auto *node : std::as_const(t&: newData.fillNodes)) { |
452 | if (nextNode) |
453 | m_rootNode->insertChildNodeBefore(node, before: nextNode); |
454 | else |
455 | m_rootNode->appendChildNode(node); |
456 | } |
457 | toBeDeleted += pathData.fillNodes; |
458 | pathData.fillNodes = newData.fillNodes; |
459 | } |
460 | if (newData.m_dirty & StrokeDirty) { |
461 | for (auto *node : std::as_const(t&: newData.strokeNodes)) { |
462 | if (nextNode) |
463 | m_rootNode->insertChildNodeBefore(node, before: nextNode); |
464 | else |
465 | m_rootNode->appendChildNode(node); |
466 | } |
467 | toBeDeleted += pathData.strokeNodes; |
468 | pathData.strokeNodes = newData.strokeNodes; |
469 | } |
470 | if (newData.m_dirty & UniformsDirty) |
471 | updateUniforms(newData); |
472 | |
473 | // Ownership of new nodes have been transferred to root node |
474 | newData.fillNodes.clear(); |
475 | newData.strokeNodes.clear(); |
476 | |
477 | #if QT_CONFIG(thread) |
478 | if (pathData.currentRunner->isAsync && (pathData.m_dirty & ~UniformsDirty)) { |
479 | // New changes have arrived while runner was computing; restart it to handle them |
480 | setUpRunner(&pathData); |
481 | QThreadPool::globalInstance()->start(runnable: pathData.currentRunner); |
482 | } else |
483 | #endif |
484 | { |
485 | pathData.currentRunner->deleteLater(); |
486 | pathData.currentRunner = nullptr; |
487 | } |
488 | } |
489 | |
490 | if (pathData.m_dirty == UniformsDirty && !pathData.currentRunner) { |
491 | // Simple case so no runner was created in endSync(); handle it directly here |
492 | updateUniforms(pathData); |
493 | pathData.m_dirty = 0; |
494 | } |
495 | } |
496 | qDeleteAll(c: toBeDeleted); // also removes them from m_rootNode's child list |
497 | } |
498 | |
499 | void QQuickShapeCurveRenderer::processPath(PathData *pathData) |
500 | { |
501 | static const bool doOverlapSolving = !qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_DISABLE_OVERLAP_SOLVER"); |
502 | static const bool doIntersetionSolving = !qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_DISABLE_INTERSECTION_SOLVER"); |
503 | static const bool useTriangulatingStroker = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_TRIANGULATING_STROKER"); |
504 | static const bool simplifyPath = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_SIMPLIFY_PATHS"); |
505 | |
506 | int &dirtyFlags = pathData->m_dirty; |
507 | |
508 | if (dirtyFlags & PathDirty) { |
509 | if (simplifyPath) |
510 | pathData->path = QQuadPath::fromPainterPath(path: pathData->originalPath.simplified(), hints: QQuadPath::PathLinear | QQuadPath::PathNonIntersecting | QQuadPath::PathNonOverlappingControlPointTriangles); |
511 | else |
512 | pathData->path = QQuadPath::fromPainterPath(path: pathData->originalPath, hints: QQuadPath::PathHints(int(pathData->pathHints))); |
513 | pathData->path.setFillRule(pathData->fillRule); |
514 | pathData->fillPath = {}; |
515 | dirtyFlags |= (FillDirty | StrokeDirty); |
516 | } |
517 | |
518 | if (dirtyFlags & FillDirty) { |
519 | if (pathData->isFillVisible()) { |
520 | if (pathData->fillPath.isEmpty()) { |
521 | pathData->fillPath = pathData->path.subPathsClosed(); |
522 | if (doIntersetionSolving) |
523 | QSGCurveProcessor::solveIntersections(path&: pathData->fillPath); |
524 | pathData->fillPath.addCurvatureData(); |
525 | if (doOverlapSolving) |
526 | QSGCurveProcessor::solveOverlaps(path&: pathData->fillPath); |
527 | } |
528 | pathData->fillNodes = addFillNodes(path: pathData->fillPath); |
529 | dirtyFlags |= (StrokeDirty | UniformsDirty); |
530 | } |
531 | } |
532 | |
533 | if (dirtyFlags & StrokeDirty) { |
534 | if (pathData->isStrokeVisible()) { |
535 | const QPen &pen = pathData->pen; |
536 | const bool solid = (pen.style() == Qt::SolidLine); |
537 | const QQuadPath &strokePath = solid ? pathData->path |
538 | : pathData->path.dashed(lineWidth: pen.widthF(), |
539 | dashPattern: pen.dashPattern(), |
540 | dashOffset: pen.dashOffset()); |
541 | if (useTriangulatingStroker) |
542 | pathData->strokeNodes = addTriangulatingStrokerNodes(path: strokePath, pen); |
543 | else |
544 | pathData->strokeNodes = addCurveStrokeNodes(path: strokePath, pen); |
545 | } |
546 | } |
547 | } |
548 | |
549 | QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addFillNodes(const QQuadPath &path) |
550 | { |
551 | NodeList ret; |
552 | std::unique_ptr<QSGCurveFillNode> node(new QSGCurveFillNode); |
553 | std::unique_ptr<QQuickShapeWireFrameNode> wfNode; |
554 | |
555 | const qsizetype approxDataCount = 20 * path.elementCount(); |
556 | node->reserve(size: approxDataCount); |
557 | |
558 | const int debugFlags = debugVisualization(); |
559 | const bool wireFrame = debugFlags & DebugWireframe; |
560 | |
561 | if (Q_LIKELY(!wireFrame)) { |
562 | QSGCurveProcessor::processFill(path, |
563 | fillRule: path.fillRule(), |
564 | addTriangle: [&node](const std::array<QVector2D, 3> &v, |
565 | const std::array<QVector2D, 3> &n, |
566 | QSGCurveProcessor::uvForPointCallback uvForPoint) |
567 | { |
568 | node->appendTriangle(v, n, uvForPoint); |
569 | }); |
570 | } else { |
571 | QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices; |
572 | wfVertices.reserve(asize: approxDataCount); |
573 | QSGCurveProcessor::processFill(path, |
574 | fillRule: path.fillRule(), |
575 | addTriangle: [&wfVertices, &node](const std::array<QVector2D, 3> &v, |
576 | const std::array<QVector2D, 3> &n, |
577 | QSGCurveProcessor::uvForPointCallback uvForPoint) |
578 | { |
579 | node->appendTriangle(v, n, uvForPoint); |
580 | |
581 | wfVertices.append(t: {.x: v.at(n: 0).x(), .y: v.at(n: 0).y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); // 0 |
582 | wfVertices.append(t: {.x: v.at(n: 1).x(), .y: v.at(n: 1).y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); // 1 |
583 | wfVertices.append(t: {.x: v.at(n: 2).x(), .y: v.at(n: 2).y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); // 2 |
584 | }); |
585 | |
586 | wfNode.reset(p: new QQuickShapeWireFrameNode); |
587 | const QVector<quint32> indices = node->uncookedIndexes(); |
588 | QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(), |
589 | wfVertices.size(), |
590 | indices.size(), |
591 | QSGGeometry::UnsignedIntType); |
592 | wfNode->setGeometry(wfg); |
593 | |
594 | wfg->setDrawingMode(QSGGeometry::DrawTriangles); |
595 | memcpy(dest: wfg->indexData(), |
596 | src: indices.data(), |
597 | n: indices.size() * wfg->sizeOfIndex()); |
598 | memcpy(dest: wfg->vertexData(), |
599 | src: wfVertices.data(), |
600 | n: wfg->vertexCount() * wfg->sizeOfVertex()); |
601 | } |
602 | |
603 | if (Q_UNLIKELY(debugFlags & DebugCurves)) |
604 | node->setDebug(0.5f); |
605 | |
606 | if (node->uncookedIndexes().size() > 0) { |
607 | node->cookGeometry(); |
608 | ret.append(t: node.release()); |
609 | if (wireFrame) |
610 | ret.append(t: wfNode.release()); |
611 | } |
612 | |
613 | return ret; |
614 | } |
615 | |
616 | QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStrokerNodes(const QQuadPath &path, const QPen &pen) |
617 | { |
618 | NodeList ret; |
619 | const QColor &color = pen.color(); |
620 | |
621 | QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices; |
622 | |
623 | QTriangulatingStroker stroker; |
624 | const auto painterPath = path.toPainterPath(); |
625 | const QVectorPath &vp = qtVectorPathForPath(path: painterPath); |
626 | stroker.process(path: vp, pen, clip: {}, hints: {}); |
627 | |
628 | auto *node = new QSGCurveFillNode; |
629 | |
630 | auto uvForPoint = [](QVector2D v1, QVector2D v2, QVector2D p) |
631 | { |
632 | double divisor = v1.x() * v2.y() - v2.x() * v1.y(); |
633 | |
634 | float u = (p.x() * v2.y() - p.y() * v2.x()) / divisor; |
635 | float v = (p.y() * v1.x() - p.x() * v1.y()) / divisor; |
636 | |
637 | return QVector2D(u, v); |
638 | }; |
639 | |
640 | // Find uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1 |
641 | // also works for a line from p0 to p2, where p1 is on the inside of the path relative to the line |
642 | auto curveUv = [uvForPoint](QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p) |
643 | { |
644 | QVector2D v1 = 2 * (p1 - p0); |
645 | QVector2D v2 = p2 - v1 - p0; |
646 | return uvForPoint(v1, v2, p - p0); |
647 | }; |
648 | |
649 | auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){ |
650 | |
651 | QVector2D baseLine = endPoint - startPoint; |
652 | QVector2D insideVector = referencePoint - startPoint; |
653 | QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()); // TODO: limit size of triangle |
654 | |
655 | bool swap = QVector2D::dotProduct(v1: insideVector, v2: normal) < 0; |
656 | |
657 | return swap ? startPoint + normal : startPoint - normal; |
658 | }; |
659 | |
660 | static bool disableExtraTriangles = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_WIP_DISABLE_EXTRA_STROKE_TRIANGLES"); |
661 | |
662 | auto addStrokeTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3){ |
663 | if (p1 == p2 || p2 == p3) { |
664 | return; |
665 | } |
666 | |
667 | auto uvForPoint = [&p1, &p2, &p3, curveUv](QVector2D p) { |
668 | auto uv = curveUv(p1, p2, p3, p); |
669 | return QVector3D(uv.x(), uv.y(), 0.0f); // Line |
670 | }; |
671 | |
672 | node->appendTriangle(v1: p1, v2: p2, v3: p3, uvForPoint); |
673 | |
674 | |
675 | wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); // 0 |
676 | wfVertices.append(t: {.x: p2.x(), .y: p2.y(), .u: 0.0f, .v: 0.1f, .w: 0.0f}); // 1 |
677 | wfVertices.append(t: {.x: p3.x(), .y: p3.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); // 2 |
678 | |
679 | if (!disableExtraTriangles) { |
680 | // Add a triangle on the outer side of the line to get some more AA |
681 | // The new point replaces p2 (currentVertex+1) |
682 | QVector2D op = findPointOtherSide(p1, p3, p2); |
683 | node->appendTriangle(v1: p1, v2: op, v3: p3, uvForPoint); |
684 | |
685 | wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); |
686 | wfVertices.append(t: {.x: op.x(), .y: op.y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); // replacing p2 |
687 | wfVertices.append(t: {.x: p3.x(), .y: p3.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); |
688 | } |
689 | }; |
690 | |
691 | const int vertCount = stroker.vertexCount() / 2; |
692 | const float *verts = stroker.vertices(); |
693 | for (int i = 0; i < vertCount - 2; ++i) { |
694 | QVector2D p[3]; |
695 | for (int j = 0; j < 3; ++j) { |
696 | p[j] = QVector2D(verts[(i+j)*2], verts[(i+j)*2 + 1]); |
697 | } |
698 | addStrokeTriangle(p[0], p[1], p[2]); |
699 | } |
700 | |
701 | QVector<quint32> indices = node->uncookedIndexes(); |
702 | if (indices.size() > 0) { |
703 | node->setColor(color); |
704 | |
705 | node->cookGeometry(); |
706 | ret.append(t: node); |
707 | } |
708 | const bool wireFrame = debugVisualization() & DebugWireframe; |
709 | if (wireFrame) { |
710 | QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode; |
711 | QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(), |
712 | wfVertices.size(), |
713 | indices.size(), |
714 | QSGGeometry::UnsignedIntType); |
715 | wfNode->setGeometry(wfg); |
716 | |
717 | wfg->setDrawingMode(QSGGeometry::DrawTriangles); |
718 | memcpy(dest: wfg->indexData(), |
719 | src: indices.data(), |
720 | n: indices.size() * wfg->sizeOfIndex()); |
721 | memcpy(dest: wfg->vertexData(), |
722 | src: wfVertices.data(), |
723 | n: wfg->vertexCount() * wfg->sizeOfVertex()); |
724 | |
725 | ret.append(t: wfNode); |
726 | } |
727 | |
728 | return ret; |
729 | } |
730 | |
731 | void QQuickShapeCurveRenderer::setRootNode(QSGNode *node) |
732 | { |
733 | clearNodeReferences(); |
734 | m_rootNode = node; |
735 | } |
736 | |
737 | void QQuickShapeCurveRenderer::clearNodeReferences() |
738 | { |
739 | for (PathData &pd : m_paths) { |
740 | pd.fillNodes.clear(); |
741 | pd.strokeNodes.clear(); |
742 | } |
743 | } |
744 | |
745 | int QQuickShapeCurveRenderer::debugVisualizationFlags = QQuickShapeCurveRenderer::NoDebug; |
746 | |
747 | int QQuickShapeCurveRenderer::debugVisualization() |
748 | { |
749 | static const int envFlags = qEnvironmentVariableIntValue(varName: "QT_QUICKSHAPES_DEBUG"); |
750 | return debugVisualizationFlags | envFlags; |
751 | } |
752 | |
753 | void QQuickShapeCurveRenderer::setDebugVisualization(int options) |
754 | { |
755 | if (debugVisualizationFlags == options) |
756 | return; |
757 | debugVisualizationFlags = options; |
758 | } |
759 | |
760 | QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addCurveStrokeNodes(const QQuadPath &path, const QPen &pen) |
761 | { |
762 | NodeList ret; |
763 | |
764 | const bool debug = debugVisualization() & DebugCurves; |
765 | auto *node = new QSGCurveStrokeNode; |
766 | node->setDebug(0.2f * debug); |
767 | QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices; |
768 | |
769 | const float penWidth = pen.widthF(); |
770 | |
771 | static const int subdivisions = qEnvironmentVariable(varName: "QT_QUICKSHAPES_STROKE_SUBDIVISIONS", QStringLiteral( "3")).toInt(); |
772 | |
773 | QSGCurveProcessor::processStroke(strokePath: path, |
774 | miterLimit: pen.miterLimit(), |
775 | penWidth, |
776 | joinStyle: pen.joinStyle(), |
777 | capStyle: pen.capStyle(), |
778 | addTriangle: [&wfVertices, &node](const std::array<QVector2D, 3> &s, |
779 | const std::array<QVector2D, 3> &p, |
780 | const std::array<QVector2D, 3> &n, |
781 | bool isLine) |
782 | { |
783 | const QVector2D &p0 = s.at(n: 0); |
784 | const QVector2D &p1 = s.at(n: 1); |
785 | const QVector2D &p2 = s.at(n: 2); |
786 | if (isLine) |
787 | node->appendTriangle(v: s, p: std::array<QVector2D, 2>{p.at(n: 0), p.at(n: 2)}, n); |
788 | else |
789 | node->appendTriangle(v: s, p, n); |
790 | |
791 | wfVertices.append(t: {.x: p0.x(), .y: p0.y(), .u: 1.0f, .v: 0.0f, .w: 0.0f}); |
792 | wfVertices.append(t: {.x: p1.x(), .y: p1.y(), .u: 0.0f, .v: 1.0f, .w: 0.0f}); |
793 | wfVertices.append(t: {.x: p2.x(), .y: p2.y(), .u: 0.0f, .v: 0.0f, .w: 1.0f}); |
794 | }, |
795 | subdivisions); |
796 | |
797 | auto indexCopy = node->uncookedIndexes(); // uncookedIndexes get delete on cooking |
798 | |
799 | node->setColor(pen.color()); |
800 | node->setStrokeWidth(penWidth); |
801 | node->cookGeometry(); |
802 | ret.append(t: node); |
803 | |
804 | const bool wireFrame = debugVisualization() & DebugWireframe; |
805 | if (wireFrame) { |
806 | QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode; |
807 | |
808 | QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(), |
809 | wfVertices.size(), |
810 | indexCopy.size(), |
811 | QSGGeometry::UnsignedIntType); |
812 | wfNode->setGeometry(wfg); |
813 | |
814 | wfg->setDrawingMode(QSGGeometry::DrawTriangles); |
815 | memcpy(dest: wfg->indexData(), |
816 | src: indexCopy.data(), |
817 | n: indexCopy.size() * wfg->sizeOfIndex()); |
818 | memcpy(dest: wfg->vertexData(), |
819 | src: wfVertices.data(), |
820 | n: wfg->vertexCount() * wfg->sizeOfVertex()); |
821 | |
822 | ret.append(t: wfNode); |
823 | } |
824 | |
825 | return ret; |
826 | } |
827 | |
828 | QT_END_NAMESPACE |
829 |
Definitions
- lcShapeCurveRenderer
- QQuickShapeWireFrameMaterialShader
- QQuickShapeWireFrameMaterialShader
- updateUniformData
- QQuickShapeWireFrameMaterial
- QQuickShapeWireFrameMaterial
- compare
- type
- createShader
- QQuickShapeWireFrameNode
- WireFrameVertex
- QQuickShapeWireFrameNode
- setColor
- activateMaterial
- attributes
- cookGeometry
- ~QQuickShapeCurveRenderer
- beginSync
- setPath
- setPath
- setStrokeColor
- setStrokeWidth
- setFillColor
- setFillRule
- setJoinStyle
- setCapStyle
- setStrokeStyle
- setFillGradient
- setFillTransform
- setFillTextureProvider
- handleSceneChange
- setAsyncCallback
- endSync
- setUpRunner
- maybeUpdateAsyncItem
- ~QQuickShapeCurveRunnable
- run
- updateNode
- processPath
- addFillNodes
- addTriangulatingStrokerNodes
- setRootNode
- clearNodeReferences
- debugVisualizationFlags
- debugVisualization
- setDebugVisualization
Learn Advanced QML with KDAB
Find out more