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