// Copyright (C) 2024 The Qt Company Ltd. // Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB). // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "extrudedtextgeometry_p.h" #include #include #include #if QT_CONFIG(concurrent) #include #endif QT_BEGIN_NAMESPACE namespace { static float edgeSplitAngle = 90.f * 0.1f; struct TriangulationData { struct Outline { int begin; int end; }; std::vector vertices; std::vector indices; std::vector outlines; std::vector outlineIndices; bool inverted; }; TriangulationData triangulate(const QString &text, const QFont &font, float scale) { TriangulationData result; int beginOutline = 0; // Initialize path with text and extract polygons QPainterPath path; path.setFillRule(Qt::WindingFill); path.addText(0, 0, font, text); QList polygons = path.toSubpathPolygons(QTransform().scale(1., -1.)); // maybe glyph has no geometry if (polygons.empty()) return result; const size_t prevNumIndices = result.indices.size(); // Reset path and add previously extracted polygons (which where spatially transformed) path = QPainterPath(); path.setFillRule(Qt::WindingFill); for (QPolygonF &p : polygons) path.addPolygon(p); // Extract polylines out of the path, this allows us to retrieve indices for each glyph outline QPolylineSet polylines = qPolyline(path); std::vector tmpIndices; tmpIndices.resize(size_t(polylines.indices.size())); memcpy(tmpIndices.data(), polylines.indices.data(), size_t(polylines.indices.size()) * sizeof(ExtrudedTextGeometry::IndexType)); int lastIndex = 0; for (const ExtrudedTextGeometry::IndexType idx : tmpIndices) { if (idx == std::numeric_limits::max()) { const int endOutline = lastIndex; result.outlines.push_back({beginOutline, endOutline}); beginOutline = endOutline; } else { result.outlineIndices.push_back(idx); ++lastIndex; } } // Triangulate path QTransform transform; transform.scale(scale, scale); const QTriangleSet triangles = qTriangulate(path, transform); // Append new indices to result.indices buffer result.indices.resize(result.indices.size() + size_t(triangles.indices.size())); memcpy(&result.indices[prevNumIndices], triangles.indices.data(), size_t(triangles.indices.size()) * sizeof(ExtrudedTextGeometry::IndexType)); for (size_t i = prevNumIndices, m = result.indices.size(); i < m; ++i) result.indices[i] += ExtrudedTextGeometry::IndexType(result.vertices.size()); // Append new triangles to result.vertices result.vertices.reserve(size_t(triangles.vertices.size()) / 2); for (qsizetype i = 0, m = triangles.vertices.size(); i < m; i += 2) result.vertices.push_back(QVector3D(triangles.vertices[i] / font.pointSizeF(), triangles.vertices[i + 1] / font.pointSizeF(), 0.0f)); return result; } inline QVector3D mix(const QVector3D &a, const QVector3D &b, float ratio) { return a + (b - a) * ratio; } } // anonymous namespace /*! \qmltype ExtrudedTextGeometry \inqmlmodule QtQuick3D.Helpers \inherits Geometry \since 6.9 \brief Provides geometry for extruded text. ExtrudedTextGeometry provides geometry for extruded text. The text is extruded along the z-axis. The text and font can be set, and the depth of the extrusion can be controlled. The size of the generated geometry is controlled by the scale and depth properties. The topology of the geometry is defined by the font.pointSize. The origin of the mesh is the rear left end of the text's baseline. */ /*! \qmlproperty string ExtrudedTextGeometry::text This property holds the text that will be extruded. */ /*! \qmlproperty font ExtrudedTextGeometry::font This property holds the font that will be used to render the text. \note The mesh geometry is normalized by the font's pointSize, so a larger pointSize will result in smoother, rather than larger, text. pixelSize should not be used. */ /*! \qmlproperty real ExtrudedTextGeometry::depth This property holds the depth of the extrusion. */ /*! \qmlproperty real ExtrudedTextGeometry::scale This property holds a scalar value of how the geometry should be scaled. This property only affects the size of the text, not the depth of the extrusion. */ /*! \qmlproperty bool ExtrudedTextGeometry::asynchronous This property holds whether the geometry generation should be asynchronous. */ /*! \qmlproperty bool ExtrudedTextGeometry::status \readonly This property holds the status of the geometry generation when asynchronous is true. \value ExtrudedTextGeometry.Null The geometry generation has not started \value ExtrudedTextGeometry.Ready The geometry generation is complete. \value ExtrudedTextGeometry.Loading The geometry generation is in progress. \value ExtrudedTextGeometry.Error The geometry generation failed. */ ExtrudedTextGeometry::ExtrudedTextGeometry(QQuick3DObject *parent) : QQuick3DGeometry(parent) { #if QT_CONFIG(concurrent) connect(&m_geometryDataWatcher, &QFutureWatcher::finished, this, &ExtrudedTextGeometry::requestFinished); #endif scheduleGeometryUpdate(); } ExtrudedTextGeometry::~ExtrudedTextGeometry() { } QString ExtrudedTextGeometry::text() const { return m_text; } void ExtrudedTextGeometry::setText(const QString &newText) { if (m_text == newText) return; m_text = newText; emit textChanged(); scheduleGeometryUpdate(); } QFont ExtrudedTextGeometry::font() const { return m_font; } void ExtrudedTextGeometry::setFont(const QFont &newFont) { if (m_font == newFont) return; m_font = newFont; emit fontChanged(); scheduleGeometryUpdate(); } float ExtrudedTextGeometry::depth() const { return m_depth; } void ExtrudedTextGeometry::setDepth(float newDepth) { if (qFuzzyCompare(m_depth, newDepth)) return; m_depth = newDepth; emit depthChanged(); scheduleGeometryUpdate(); } float ExtrudedTextGeometry::scale() const { return m_scale; } void ExtrudedTextGeometry::setScale(float newScale) { if (qFuzzyCompare(m_scale, newScale)) return; m_scale = newScale; emit scaleChanged(); scheduleGeometryUpdate(); } bool ExtrudedTextGeometry::asynchronous() const { return m_asynchronous; } void ExtrudedTextGeometry::setAsynchronous(bool newAsynchronous) { if (m_asynchronous == newAsynchronous) return; m_asynchronous = newAsynchronous; emit asynchronousChanged(); } ExtrudedTextGeometry::Status ExtrudedTextGeometry::status() const { return m_status; } void ExtrudedTextGeometry::doUpdateGeometry() { // reset the flag since we are processing the update m_geometryUpdateRequested = false; #if QT_CONFIG(concurrent) if (m_geometryDataFuture.isRunning()) { m_pendingAsyncUpdate = true; return; } #endif // If text is empty, clear the geometry // Note this happens after we check if we are already running an update // asynchronously. if (m_text.isEmpty()) { clear(); update(); return; } #if QT_CONFIG(concurrent) if (m_asynchronous) { m_geometryDataFuture = QtConcurrent::run(generateExtrudedTextGeometryAsync, m_text, m_font, m_depth, m_scale); m_geometryDataWatcher.setFuture(m_geometryDataFuture); m_status = Status::Loading; Q_EMIT statusChanged(); } else { #else { #endif // QT_CONFIG(concurrent) updateGeometry(generateExtrudedTextGeometry(m_text, m_font, m_depth, m_scale)); } } void ExtrudedTextGeometry::requestFinished() { #if QT_CONFIG(concurrent) const auto output = m_geometryDataFuture.takeResult(); updateGeometry(output); #endif } void ExtrudedTextGeometry::scheduleGeometryUpdate() { if (!m_geometryUpdateRequested) { QMetaObject::invokeMethod(this, "doUpdateGeometry", Qt::QueuedConnection); m_geometryUpdateRequested = true; } } void ExtrudedTextGeometry::updateGeometry(const GeometryData &geometryData) { // clear(); setStride(sizeof(float) * 6); // 3 for position, 3 for normal setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles); addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type); addAttribute(QQuick3DGeometry::Attribute::NormalSemantic, 3 * sizeof(float), QQuick3DGeometry::Attribute::F32Type); addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0, QQuick3DGeometry::Attribute::U32Type); setBounds(geometryData.boundsMin, geometryData.boundsMax); setVertexData(geometryData.vertexData); setIndexData(geometryData.indexData); // If the geometry update was requested while the geometry was being generated asynchronously, // we need to schedule another geometry update now that the geometry is ready. if (m_pendingAsyncUpdate) { m_pendingAsyncUpdate = false; scheduleGeometryUpdate(); } else { m_status = Status::Ready; Q_EMIT statusChanged(); } update(); } ExtrudedTextGeometry::GeometryData ExtrudedTextGeometry::generateExtrudedTextGeometry(const QString &text, const QFont &font, float depth, float scale) { GeometryData output; struct Vertex { QVector3D position; QVector3D normal; }; std::vector indices; std::vector vertices; TriangulationData data = triangulate(text, font, scale); const IndexType numVertices = IndexType(data.vertices.size()); const size_t numIndices = data.indices.size(); vertices.reserve(data.vertices.size() * 2); for (QVector3D &v : data.vertices) // front face vertices.push_back({ v, QVector3D(0.0f, 0.0f, -1.0f) }); for (QVector3D &v : data.vertices) // back face vertices.push_back({ QVector3D(v.x(), v.y(), depth), QVector3D(0.0f, 0.0f, 1.0f) }); int verticesIndex = int(vertices.size()); for (size_t i = 0; i < data.outlines.size(); ++i) { const int begin = data.outlines[i].begin; const int end = data.outlines[i].end; const int verticesIndexBegin = verticesIndex; if (begin == end) continue; QVector3D prevNormal = QVector3D::crossProduct( vertices[data.outlineIndices[end - 1] + numVertices].position - vertices[data.outlineIndices[end - 1]].position, vertices[data.outlineIndices[begin]].position - vertices[data.outlineIndices[end - 1]].position).normalized(); for (int j = begin; j < end; ++j) { const bool isLastIndex = (j == end - 1); const IndexType cur = data.outlineIndices[j]; const IndexType next = data.outlineIndices[((j - begin + 1) % (end - begin)) + begin]; // normalize, bring in range and adjust const QVector3D normal = QVector3D::crossProduct(vertices[cur + numVertices].position - vertices[cur].position, vertices[next].position - vertices[cur].position).normalized(); // use smooth normals in case of a short angle const bool smooth = QVector3D::dotProduct(prevNormal, normal) > (90.0f - edgeSplitAngle) / 90.0f; const QVector3D resultNormal = smooth ? mix(prevNormal, normal, 0.5f) : normal; if (!smooth) { vertices.push_back({vertices[cur].position, prevNormal}); vertices.push_back({vertices[cur + numVertices].position, prevNormal}); verticesIndex += 2; } vertices.push_back({vertices[cur].position, resultNormal}); vertices.push_back({vertices[cur + numVertices].position, resultNormal}); const int v0 = verticesIndex; const int v1 = verticesIndex + 1; const int v2 = isLastIndex ? verticesIndexBegin : verticesIndex + 2; const int v3 = isLastIndex ? verticesIndexBegin + 1 : verticesIndex + 3; indices.push_back(v0); indices.push_back(v1); indices.push_back(v2); indices.push_back(v2); indices.push_back(v1); indices.push_back(v3); verticesIndex += 2; prevNormal = normal; } } // Indices for the front and back faces const int indicesOffset = int(indices.size()); indices.resize(indices.size() + numIndices * 2); // copy values for back faces IndexType *indicesFaces = indices.data() + indicesOffset; memcpy(indicesFaces, data.indices.data(), numIndices * sizeof(IndexType)); // insert values for front face and flip triangles for (size_t j = 0; j < numIndices; j += 3) { indicesFaces[numIndices + j ] = indicesFaces[j ] + numVertices; indicesFaces[numIndices + j + 1] = indicesFaces[j + 2] + numVertices; indicesFaces[numIndices + j + 2] = indicesFaces[j + 1] + numVertices; } for (const auto &vertex : vertices) { const auto &p = vertex.position; output.boundsMin = QVector3D(qMin(output.boundsMin.x(), p.x()), qMin(output.boundsMin.y(), p.y()), qMin(output.boundsMin.z(), p.z())); output.boundsMax = QVector3D(qMax(output.boundsMax.x(), p.x()), qMax(output.boundsMax.y(), p.y()), qMax(output.boundsMax.z(), p.z())); } output.vertexData.resize(vertices.size() * sizeof(Vertex)); memcpy(output.vertexData.data(), vertices.data(), vertices.size() * sizeof(Vertex)); output.indexData.resize(indices.size() * sizeof(IndexType)); memcpy(output.indexData.data(), indices.data(), indices.size() * sizeof(IndexType)); return output; } #if QT_CONFIG(concurrent) void ExtrudedTextGeometry::generateExtrudedTextGeometryAsync(QPromise &promise, const QString &text, const QFont &font, float depth, float scale) { GeometryData output = generateExtrudedTextGeometry(text, font, depth, scale); promise.addResult(output); } #endif QT_END_NAMESPACE