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