Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
extrudedtextgeometry.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
4
6#include <QPainterPath>
7#include <QtGui/private/qtriangulator_p.h>
8#include <QVector3D>
9
10#if QT_CONFIG(concurrent)
11#include <QtConcurrentRun>
12#endif
13
15
16namespace {
17
18static float edgeSplitAngle = 90.f * 0.1f;
19
32
33TriangulationData triangulate(const QString &text, const QFont &font, float scale)
34{
35 TriangulationData result;
36 int beginOutline = 0;
37
38 // Initialize path with text and extract polygons
39 QPainterPath path;
40 path.setFillRule(Qt::WindingFill);
41 path.addText(0, 0, font, text);
42 QList<QPolygonF> polygons = path.toSubpathPolygons(QTransform().scale(1., -1.));
43
44 // maybe glyph has no geometry
45 if (polygons.empty())
46 return result;
47
48 const size_t prevNumIndices = result.indices.size();
49
50 // Reset path and add previously extracted polygons (which where spatially transformed)
51 path = QPainterPath();
52 path.setFillRule(Qt::WindingFill);
53 for (QPolygonF &p : polygons)
54 path.addPolygon(p);
55
56 // Extract polylines out of the path, this allows us to retrieve indices for each glyph outline
57 QPolylineSet polylines = qPolyline(path);
58 std::vector<ExtrudedTextGeometry::IndexType> tmpIndices;
59 tmpIndices.resize(size_t(polylines.indices.size()));
60 memcpy(tmpIndices.data(), polylines.indices.data(), size_t(polylines.indices.size()) * sizeof(ExtrudedTextGeometry::IndexType));
61
62 int lastIndex = 0;
63 for (const ExtrudedTextGeometry::IndexType idx : tmpIndices) {
64 if (idx == std::numeric_limits<ExtrudedTextGeometry::IndexType>::max()) {
65 const int endOutline = lastIndex;
66 result.outlines.push_back({beginOutline, endOutline});
67 beginOutline = endOutline;
68 } else {
69 result.outlineIndices.push_back(idx);
70 ++lastIndex;
71 }
72 }
73
74 // Triangulate path
75 QTransform transform;
76 transform.scale(scale, scale);
77 const QTriangleSet triangles = qTriangulate(path, transform);
78
79 // Append new indices to result.indices buffer
80 result.indices.resize(result.indices.size() + size_t(triangles.indices.size()));
81 memcpy(&result.indices[prevNumIndices], triangles.indices.data(), size_t(triangles.indices.size()) * sizeof(ExtrudedTextGeometry::IndexType));
82 for (size_t i = prevNumIndices, m = result.indices.size(); i < m; ++i)
83 result.indices[i] += ExtrudedTextGeometry::IndexType(result.vertices.size());
84
85 // Append new triangles to result.vertices
86 result.vertices.reserve(size_t(triangles.vertices.size()) / 2);
87 for (qsizetype i = 0, m = triangles.vertices.size(); i < m; i += 2)
88 result.vertices.push_back(QVector3D(triangles.vertices[i] / font.pointSizeF(), triangles.vertices[i + 1] / font.pointSizeF(), 0.0f));
89
90 return result;
91}
92
93inline QVector3D mix(const QVector3D &a, const QVector3D &b, float ratio)
94{
95 return a + (b - a) * ratio;
96}
97
98} // anonymous namespace
99
100
101/*!
102 \qmltype ExtrudedTextGeometry
103 \inqmlmodule QtQuick3D.Helpers
104 \inherits Geometry
105 \since 6.9
106 \brief Provides geometry for extruded text.
107
108 ExtrudedTextGeometry provides geometry for extruded text. The text is extruded along the z-axis.
109 The text and font can be set, and the depth of the extrusion can be controlled.
110 The size of the generated geometry is controlled by the scale and depth properties. The topology
111 of the geometry is defined by the font.pointSize.
112
113 The origin of the mesh is the rear left end of the text's baseline.
114*/
115
116/*!
117 \qmlproperty string ExtrudedTextGeometry::text
118
119 This property holds the text that will be extruded.
120*/
121
122/*!
123 \qmlproperty font ExtrudedTextGeometry::font
124
125 This property holds the font that will be used to render the text.
126
127 \note The mesh geometry is normalized by the font's pointSize, so a larger pointSize
128 will result in smoother, rather than larger, text. pixelSize should not
129 be used.
130*/
131
132/*!
133 \qmlproperty real ExtrudedTextGeometry::depth
134
135 This property holds the depth of the extrusion.
136*/
137
138/*!
139 \qmlproperty real ExtrudedTextGeometry::scale
140
141 This property holds a scalar value of how the geometry should be scaled.
142 This property only affects the size of the text, not the depth of the extrusion.
143*/
144
145/*!
146 \qmlproperty bool ExtrudedTextGeometry::asynchronous
147
148 This property holds whether the geometry generation should be asynchronous.
149*/
150
151/*!
152 \qmlproperty bool ExtrudedTextGeometry::status
153 \readonly
154
155 This property holds the status of the geometry generation when asynchronous is true.
156
157 \value ExtrudedTextGeometry.Null The geometry generation has not started
158 \value ExtrudedTextGeometry.Ready The geometry generation is complete.
159 \value ExtrudedTextGeometry.Loading The geometry generation is in progress.
160 \value ExtrudedTextGeometry.Error The geometry generation failed.
161*/
162
163
164ExtrudedTextGeometry::ExtrudedTextGeometry(QQuick3DObject *parent)
165 : QQuick3DGeometry(parent)
166{
167#if QT_CONFIG(concurrent)
168 connect(&m_geometryDataWatcher, &QFutureWatcher<GeometryData>::finished, this, &ExtrudedTextGeometry::requestFinished);
169#endif
170 scheduleGeometryUpdate();
171}
172
173ExtrudedTextGeometry::~ExtrudedTextGeometry()
174{
175
176}
177
178QString ExtrudedTextGeometry::text() const
179{
180 return m_text;
181}
182
183void ExtrudedTextGeometry::setText(const QString &newText)
184{
185 if (m_text == newText)
186 return;
187 m_text = newText;
188 emit textChanged();
189 scheduleGeometryUpdate();
190}
191
192QFont ExtrudedTextGeometry::font() const
193{
194 return m_font;
195}
196
197void ExtrudedTextGeometry::setFont(const QFont &newFont)
198{
199 if (m_font == newFont)
200 return;
201 m_font = newFont;
202 emit fontChanged();
203 scheduleGeometryUpdate();
204}
205
206float ExtrudedTextGeometry::depth() const
207{
208 return m_depth;
209}
210
211void ExtrudedTextGeometry::setDepth(float newDepth)
212{
213 if (qFuzzyCompare(m_depth, newDepth))
214 return;
215 m_depth = newDepth;
216 emit depthChanged();
217 scheduleGeometryUpdate();
218}
219
220float ExtrudedTextGeometry::scale() const
221{
222 return m_scale;
223}
224
225void ExtrudedTextGeometry::setScale(float newScale)
226{
227 if (qFuzzyCompare(m_scale, newScale))
228 return;
229 m_scale = newScale;
230 emit scaleChanged();
231 scheduleGeometryUpdate();
232}
233
234bool ExtrudedTextGeometry::asynchronous() const
235{
236 return m_asynchronous;
237}
238
239void ExtrudedTextGeometry::setAsynchronous(bool newAsynchronous)
240{
241 if (m_asynchronous == newAsynchronous)
242 return;
243 m_asynchronous = newAsynchronous;
244 emit asynchronousChanged();
245}
246
247ExtrudedTextGeometry::Status ExtrudedTextGeometry::status() const
248{
249 return m_status;
250}
251
252void ExtrudedTextGeometry::doUpdateGeometry()
253{
254 // reset the flag since we are processing the update
255 m_geometryUpdateRequested = false;
256
257#if QT_CONFIG(concurrent)
258 if (m_geometryDataFuture.isRunning()) {
259 m_pendingAsyncUpdate = true;
260 return;
261 }
262#endif
263
264 // If text is empty, clear the geometry
265 // Note this happens after we check if we are already running an update
266 // asynchronously.
267 if (m_text.isEmpty()) {
268 clear();
269 update();
270 return;
271 }
272
273#if QT_CONFIG(concurrent)
274
275 if (m_asynchronous) {
276 m_geometryDataFuture = QtConcurrent::run(generateExtrudedTextGeometryAsync,
277 m_text,
278 m_font,
279 m_depth,
280 m_scale);
281 m_geometryDataWatcher.setFuture(m_geometryDataFuture);
282 m_status = Status::Loading;
283 Q_EMIT statusChanged();
284 } else {
285#else
286 {
287
288#endif // QT_CONFIG(concurrent)
289 updateGeometry(generateExtrudedTextGeometry(m_text, m_font, m_depth, m_scale));
290 }
291}
292
293void ExtrudedTextGeometry::requestFinished()
294{
295#if QT_CONFIG(concurrent)
296 const auto output = m_geometryDataFuture.takeResult();
297 updateGeometry(output);
298#endif
299}
300
301void ExtrudedTextGeometry::scheduleGeometryUpdate()
302{
303 if (!m_geometryUpdateRequested) {
304 QMetaObject::invokeMethod(this, "doUpdateGeometry", Qt::QueuedConnection);
305 m_geometryUpdateRequested = true;
306 }
307}
308
309void ExtrudedTextGeometry::updateGeometry(const GeometryData &geometryData)
310{
311 // clear();
312 setStride(sizeof(float) * 6); // 3 for position, 3 for normal
313 setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
314 addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
315 0,
316 QQuick3DGeometry::Attribute::F32Type);
317 addAttribute(QQuick3DGeometry::Attribute::NormalSemantic,
318 3 * sizeof(float),
319 QQuick3DGeometry::Attribute::F32Type);
320 addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
321 0,
322 QQuick3DGeometry::Attribute::U32Type);
323
324 setBounds(geometryData.boundsMin, geometryData.boundsMax);
325 setVertexData(geometryData.vertexData);
326 setIndexData(geometryData.indexData);
327
328 // If the geometry update was requested while the geometry was being generated asynchronously,
329 // we need to schedule another geometry update now that the geometry is ready.
330 if (m_pendingAsyncUpdate) {
331 m_pendingAsyncUpdate = false;
332 scheduleGeometryUpdate();
333 } else {
334 m_status = Status::Ready;
335 Q_EMIT statusChanged();
336 }
337 update();
338}
339
340ExtrudedTextGeometry::GeometryData ExtrudedTextGeometry::generateExtrudedTextGeometry(const QString &text,
341 const QFont &font,
342 float depth,
343 float scale)
344{
345 GeometryData output;
346
347 struct Vertex {
348 QVector3D position;
349 QVector3D normal;
350 };
351
352 std::vector<IndexType> indices;
353 std::vector<Vertex> vertices;
354
355 TriangulationData data = triangulate(text, font, scale);
356
357 const IndexType numVertices = IndexType(data.vertices.size());
358 const size_t numIndices = data.indices.size();
359
360 vertices.reserve(data.vertices.size() * 2);
361 for (QVector3D &v : data.vertices) // front face
362 vertices.push_back({ v, QVector3D(0.0f, 0.0f, -1.0f) });
363 for (QVector3D &v : data.vertices) // back face
364 vertices.push_back({ QVector3D(v.x(), v.y(), depth), QVector3D(0.0f, 0.0f, 1.0f) });
365
366 int verticesIndex = int(vertices.size());
367 for (size_t i = 0; i < data.outlines.size(); ++i) {
368 const int begin = data.outlines[i].begin;
369 const int end = data.outlines[i].end;
370 const int verticesIndexBegin = verticesIndex;
371
372 if (begin == end)
373 continue;
374
375 QVector3D prevNormal = QVector3D::crossProduct(
376 vertices[data.outlineIndices[end - 1] + numVertices].position - vertices[data.outlineIndices[end - 1]].position,
377 vertices[data.outlineIndices[begin]].position - vertices[data.outlineIndices[end - 1]].position).normalized();
378
379 for (int j = begin; j < end; ++j) {
380 const bool isLastIndex = (j == end - 1);
381 const IndexType cur = data.outlineIndices[j];
382 const IndexType next = data.outlineIndices[((j - begin + 1) % (end - begin)) + begin]; // normalize, bring in range and adjust
383 const QVector3D normal = QVector3D::crossProduct(vertices[cur + numVertices].position - vertices[cur].position, vertices[next].position - vertices[cur].position).normalized();
384
385 // use smooth normals in case of a short angle
386 const bool smooth = QVector3D::dotProduct(prevNormal, normal) > (90.0f - edgeSplitAngle) / 90.0f;
387 const QVector3D resultNormal = smooth ? mix(prevNormal, normal, 0.5f) : normal;
388 if (!smooth) {
389 vertices.push_back({vertices[cur].position, prevNormal});
390 vertices.push_back({vertices[cur + numVertices].position, prevNormal});
391 verticesIndex += 2;
392 }
393
394 vertices.push_back({vertices[cur].position, resultNormal});
395 vertices.push_back({vertices[cur + numVertices].position, resultNormal});
396
397 const int v0 = verticesIndex;
398 const int v1 = verticesIndex + 1;
399 const int v2 = isLastIndex ? verticesIndexBegin : verticesIndex + 2;
400 const int v3 = isLastIndex ? verticesIndexBegin + 1 : verticesIndex + 3;
401
402 indices.push_back(v0);
403 indices.push_back(v1);
404 indices.push_back(v2);
405 indices.push_back(v2);
406 indices.push_back(v1);
407 indices.push_back(v3);
408
409 verticesIndex += 2;
410 prevNormal = normal;
411 }
412 }
413
414 // Indices for the front and back faces
415 const int indicesOffset = int(indices.size());
416 indices.resize(indices.size() + numIndices * 2);
417
418 // copy values for back faces
419 IndexType *indicesFaces = indices.data() + indicesOffset;
420 memcpy(indicesFaces, data.indices.data(), numIndices * sizeof(IndexType));
421
422 // insert values for front face and flip triangles
423 for (size_t j = 0; j < numIndices; j += 3) {
424 indicesFaces[numIndices + j ] = indicesFaces[j ] + numVertices;
425 indicesFaces[numIndices + j + 1] = indicesFaces[j + 2] + numVertices;
426 indicesFaces[numIndices + j + 2] = indicesFaces[j + 1] + numVertices;
427 }
428
429 for (const auto &vertex : vertices) {
430 const auto &p = vertex.position;
431 output.boundsMin = QVector3D(qMin(output.boundsMin.x(), p.x()), qMin(output.boundsMin.y(), p.y()), qMin(output.boundsMin.z(), p.z()));
432 output.boundsMax = QVector3D(qMax(output.boundsMax.x(), p.x()), qMax(output.boundsMax.y(), p.y()), qMax(output.boundsMax.z(), p.z()));
433 }
434
435
436 output.vertexData.resize(vertices.size() * sizeof(Vertex));
437 memcpy(output.vertexData.data(), vertices.data(), vertices.size() * sizeof(Vertex));
438
439
440 output.indexData.resize(indices.size() * sizeof(IndexType));
441 memcpy(output.indexData.data(), indices.data(), indices.size() * sizeof(IndexType));
442
443
444 return output;
445}
446#if QT_CONFIG(concurrent)
447void ExtrudedTextGeometry::generateExtrudedTextGeometryAsync(QPromise<GeometryData> &promise,
448 const QString &text,
449 const QFont &font,
450 float depth,
451 float scale)
452{
453 GeometryData output = generateExtrudedTextGeometry(text, font, depth, scale);
454 promise.addResult(output);
455}
456#endif
457
458QT_END_NAMESPACE
QVector3D mix(const QVector3D &a, const QVector3D &b, float ratio)
TriangulationData triangulate(const QString &text, const QFont &font, float scale)
std::vector< ExtrudedTextGeometry::IndexType > indices
std::vector< ExtrudedTextGeometry::IndexType > outlineIndices