Revision control

++ // 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
++ // Qt-Security score:significant reason:default
++
++
++ #include "extrudedtextgeometry_p.h"
++ #include <QPainterPath>
++ #include <QtGui/private/qtriangulator_p.h>
++ #include <QVector3D>
++
++ #if QT_CONFIG(concurrent)
++ #include <QtConcurrentRun>
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ namespace {
++
++ static float edgeSplitAngle = 90.f * 0.1f;
++
++ struct TriangulationData {
++ struct Outline {
++ int begin;
++ int end;
++ };
++
++ std::vector<QVector3D> vertices;
++ std::vector<ExtrudedTextGeometry::IndexType> indices;
++ std::vector<Outline> outlines;
++ std::vector<ExtrudedTextGeometry::IndexType> 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<QPolygonF> 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<ExtrudedTextGeometry::IndexType> 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<ExtrudedTextGeometry::IndexType>::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<GeometryData>::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();
++ emit geometryChanged();
++ }
++
++ ExtrudedTextGeometry::GeometryData ExtrudedTextGeometry::generateExtrudedTextGeometry(const QString &text,
++ const QFont &font,
++ float depth,
++ float scale)
++ {
++ GeometryData output;
++
++ struct Vertex {
++ QVector3D position;
++ QVector3D normal;
++ };
++
++ std::vector<IndexType> indices;
++ std::vector<Vertex> 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<GeometryData> &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