| 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 = 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 | |