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 if (m_asynchronous) {
278 m_geometryDataFuture = QtConcurrent::run(generateExtrudedTextGeometryAsync,
279 m_text,
280 m_font,
281 m_depth,
282 m_scale);
283 m_geometryDataWatcher.setFuture(m_geometryDataFuture);
284 m_status = Status::Loading;
285 Q_EMIT statusChanged();
286 } else {
287#else
288 {
289
290#endif // QT_CONFIG(concurrent)
291 updateGeometry(generateExtrudedTextGeometry(m_text, m_font, m_depth, m_scale));
292 }
293}
294
295void ExtrudedTextGeometry::requestFinished()
296{
297#if QT_CONFIG(concurrent)
298 const auto output = m_geometryDataFuture.takeResult();
299 updateGeometry(output);
300#endif
301}
302
303void ExtrudedTextGeometry::scheduleGeometryUpdate()
304{
305 if (!m_geometryUpdateRequested) {
306 QMetaObject::invokeMethod(this, "doUpdateGeometry", Qt::QueuedConnection);
307 m_geometryUpdateRequested = true;
308 }
309}
310
311void ExtrudedTextGeometry::updateGeometry(const GeometryData &geometryData)
312{
313 // clear();
314 setStride(sizeof(float) * 6); // 3 for position, 3 for normal
315 setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
316 addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
317 0,
318 QQuick3DGeometry::Attribute::F32Type);
319 addAttribute(QQuick3DGeometry::Attribute::NormalSemantic,
320 3 * sizeof(float),
321 QQuick3DGeometry::Attribute::F32Type);
322 addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
323 0,
324 QQuick3DGeometry::Attribute::U32Type);
325
326 setBounds(geometryData.boundsMin, geometryData.boundsMax);
327 setVertexData(geometryData.vertexData);
328 setIndexData(geometryData.indexData);
329
330 // If the geometry update was requested while the geometry was being generated asynchronously,
331 // we need to schedule another geometry update now that the geometry is ready.
332 if (m_pendingAsyncUpdate) {
333 m_pendingAsyncUpdate = false;
334 scheduleGeometryUpdate();
335 } else {
336 m_status = Status::Ready;
337 Q_EMIT statusChanged();
338 }
339 update();
340 emit geometryChanged();
341}
342
343ExtrudedTextGeometry::GeometryData ExtrudedTextGeometry::generateExtrudedTextGeometry(const QString &text,
344 const QFont &font,
345 float depth,
346 float scale)
347{
348 GeometryData output;
349
350 struct Vertex {
351 QVector3D position;
352 QVector3D normal;
353 };
354
355 std::vector<IndexType> indices;
356 std::vector<Vertex> vertices;
357
358 TriangulationData data = triangulate(text, font, scale);
359
360 const IndexType numVertices = IndexType(data.vertices.size());
361 const size_t numIndices = data.indices.size();
362
363 vertices.reserve(data.vertices.size() * 2);
364 for (QVector3D &v : data.vertices) // front face
365 vertices.push_back({ v, QVector3D(0.0f, 0.0f, -1.0f) });
366 for (QVector3D &v : data.vertices) // back face
367 vertices.push_back({ QVector3D(v.x(), v.y(), depth), QVector3D(0.0f, 0.0f, 1.0f) });
368
369 int verticesIndex = int(vertices.size());
370 for (size_t i = 0; i < data.outlines.size(); ++i) {
371 const int begin = data.outlines[i].begin;
372 const int end = data.outlines[i].end;
373 const int verticesIndexBegin = verticesIndex;
374
375 if (begin == end)
376 continue;
377
378 QVector3D prevNormal = QVector3D::crossProduct(
379 vertices[data.outlineIndices[end - 1] + numVertices].position - vertices[data.outlineIndices[end - 1]].position,
380 vertices[data.outlineIndices[begin]].position - vertices[data.outlineIndices[end - 1]].position).normalized();
381
382 for (int j = begin; j < end; ++j) {
383 const bool isLastIndex = (j == end - 1);
384 const IndexType cur = data.outlineIndices[j];
385 const IndexType next = data.outlineIndices[((j - begin + 1) % (end - begin)) + begin]; // normalize, bring in range and adjust
386 const QVector3D normal = QVector3D::crossProduct(vertices[cur + numVertices].position - vertices[cur].position, vertices[next].position - vertices[cur].position).normalized();
387
388 // use smooth normals in case of a short angle
389 const bool smooth = QVector3D::dotProduct(prevNormal, normal) > (90.0f - edgeSplitAngle) / 90.0f;
390 const QVector3D resultNormal = smooth ? mix(prevNormal, normal, 0.5f) : normal;
391 if (!smooth) {
392 vertices.push_back({vertices[cur].position, prevNormal});
393 vertices.push_back({vertices[cur + numVertices].position, prevNormal});
394 verticesIndex += 2;
395 }
396
397 vertices.push_back({vertices[cur].position, resultNormal});
398 vertices.push_back({vertices[cur + numVertices].position, resultNormal});
399
400 const int v0 = verticesIndex;
401 const int v1 = verticesIndex + 1;
402 const int v2 = isLastIndex ? verticesIndexBegin : verticesIndex + 2;
403 const int v3 = isLastIndex ? verticesIndexBegin + 1 : verticesIndex + 3;
404
405 indices.push_back(v0);
406 indices.push_back(v1);
407 indices.push_back(v2);
408 indices.push_back(v2);
409 indices.push_back(v1);
410 indices.push_back(v3);
411
412 verticesIndex += 2;
413 prevNormal = normal;
414 }
415 }
416
417 // Indices for the front and back faces
418 const int indicesOffset = int(indices.size());
419 indices.resize(indices.size() + numIndices * 2);
420
421 // copy values for back faces
422 IndexType *indicesFaces = indices.data() + indicesOffset;
423 memcpy(indicesFaces, data.indices.data(), numIndices * sizeof(IndexType));
424
425 // insert values for front face and flip triangles
426 for (size_t j = 0; j < numIndices; j += 3) {
427 indicesFaces[numIndices + j ] = indicesFaces[j ] + numVertices;
428 indicesFaces[numIndices + j + 1] = indicesFaces[j + 2] + numVertices;
429 indicesFaces[numIndices + j + 2] = indicesFaces[j + 1] + numVertices;
430 }
431
432 for (const auto &vertex : vertices) {
433 const auto &p = vertex.position;
434 output.boundsMin = QVector3D(qMin(output.boundsMin.x(), p.x()), qMin(output.boundsMin.y(), p.y()), qMin(output.boundsMin.z(), p.z()));
435 output.boundsMax = QVector3D(qMax(output.boundsMax.x(), p.x()), qMax(output.boundsMax.y(), p.y()), qMax(output.boundsMax.z(), p.z()));
436 }
437
438
439 output.vertexData.resize(vertices.size() * sizeof(Vertex));
440 memcpy(output.vertexData.data(), vertices.data(), vertices.size() * sizeof(Vertex));
441
442
443 output.indexData.resize(indices.size() * sizeof(IndexType));
444 memcpy(output.indexData.data(), indices.data(), indices.size() * sizeof(IndexType));
445
446
447 return output;
448}
449#if QT_CONFIG(concurrent)
450void ExtrudedTextGeometry::generateExtrudedTextGeometryAsync(QPromise<GeometryData> &promise,
451 const QString &text,
452 const QFont &font,
453 float depth,
454 float scale)
455{
456 GeometryData output = generateExtrudedTextGeometry(text, font, depth, scale);
457 promise.addResult(output);
458}
459#endif
460
461QT_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