1// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
4#include "qextrudedtextgeometry.h"
5#include "qextrudedtextgeometry_p.h"
6#include <Qt3DCore/qbuffer.h>
7#include <Qt3DCore/qattribute.h>
8#include <private/qtriangulator_p.h>
9#include <qmath.h>
10#include <QVector3D>
11#include <QTextLayout>
12#include <QTime>
13#include <QPainterPath>
14
15QT_BEGIN_NAMESPACE
16
17namespace Qt3DExtras {
18
19namespace {
20
21static float edgeSplitAngle = 90.f * 0.1f;
22
23using IndexType = unsigned int;
24
25struct TriangulationData {
26 struct Outline {
27 int begin;
28 int end;
29 };
30
31 std::vector<QVector3D> vertices;
32 std::vector<IndexType> indices;
33 std::vector<Outline> outlines;
34 std::vector<IndexType> outlineIndices;
35 bool inverted;
36};
37
38TriangulationData triangulate(const QString &text, const QFont &font)
39{
40 TriangulationData result;
41 int beginOutline = 0;
42
43 // Initialize path with text and extract polygons
44 QPainterPath path;
45 path.setFillRule(Qt::WindingFill);
46 path.addText(x: 0, y: 0, f: font, text);
47 QList<QPolygonF> polygons = path.toSubpathPolygons(matrix: QTransform().scale(sx: 1., sy: -1.));
48
49 // maybe glyph has no geometry
50 if (polygons.size() == 0)
51 return result;
52
53 const size_t prevNumIndices = result.indices.size();
54
55 // Reset path and add previously extracted polygons (which where spatially transformed)
56 path = QPainterPath();
57 path.setFillRule(Qt::WindingFill);
58 for (QPolygonF &p : polygons)
59 path.addPolygon(polygon: p);
60
61 // Extract polylines out of the path, this allows us to retrieve indices for each glyph outline
62 QPolylineSet polylines = qPolyline(path);
63 std::vector<IndexType> tmpIndices;
64 tmpIndices.resize(new_size: size_t(polylines.indices.size()));
65 memcpy(dest: tmpIndices.data(), src: polylines.indices.data(), n: size_t(polylines.indices.size()) * sizeof(IndexType));
66
67 int lastIndex = 0;
68 for (const IndexType idx : tmpIndices) {
69 if (idx == std::numeric_limits<IndexType>::max()) {
70 const int endOutline = lastIndex;
71 result.outlines.push_back(x: {.begin: beginOutline, .end: endOutline});
72 beginOutline = endOutline;
73 } else {
74 result.outlineIndices.push_back(x: idx);
75 ++lastIndex;
76 }
77 }
78
79 // Triangulate path
80 const QTriangleSet triangles = qTriangulate(path);
81
82 // Append new indices to result.indices buffer
83 result.indices.resize(new_size: result.indices.size() + size_t(triangles.indices.size()));
84 memcpy(dest: &result.indices[prevNumIndices], src: triangles.indices.data(), n: size_t(triangles.indices.size()) * sizeof(IndexType));
85 for (size_t i = prevNumIndices, m = result.indices.size(); i < m; ++i)
86 result.indices[i] += IndexType(result.vertices.size());
87
88 // Append new triangles to result.vertices
89 result.vertices.reserve(n: size_t(triangles.vertices.size()) / 2);
90 for (qsizetype i = 0, m = triangles.vertices.size(); i < m; i += 2)
91 result.vertices.push_back(x: QVector3D(triangles.vertices[i] / font.pointSizeF(), triangles.vertices[i + 1] / font.pointSizeF(), 0.0f));
92
93 return result;
94}
95
96inline QVector3D mix(const QVector3D &a, const QVector3D &b, float ratio)
97{
98 return a + (b - a) * ratio;
99}
100
101} // anonymous namespace
102
103QExtrudedTextGeometryPrivate::QExtrudedTextGeometryPrivate()
104 : QGeometryPrivate()
105 , m_font(QFont(QStringLiteral("Arial")))
106 , m_depth(1.f)
107 , m_positionAttribute(nullptr)
108 , m_normalAttribute(nullptr)
109 , m_indexAttribute(nullptr)
110 , m_vertexBuffer(nullptr)
111 , m_indexBuffer(nullptr)
112{
113 m_font.setPointSize(4);
114}
115
116void QExtrudedTextGeometryPrivate::init()
117{
118 Q_Q(QExtrudedTextGeometry);
119 m_positionAttribute = new Qt3DCore::QAttribute(q);
120 m_normalAttribute = new Qt3DCore::QAttribute(q);
121 m_indexAttribute = new Qt3DCore::QAttribute(q);
122 m_vertexBuffer = new Qt3DCore::QBuffer(q);
123 m_indexBuffer = new Qt3DCore::QBuffer(q);
124
125 const quint32 elementSize = 3 + 3;
126 const quint32 stride = elementSize * sizeof(float);
127
128 m_positionAttribute->setName(Qt3DCore::QAttribute::defaultPositionAttributeName());
129 m_positionAttribute->setVertexBaseType(Qt3DCore::QAttribute::Float);
130 m_positionAttribute->setVertexSize(3);
131 m_positionAttribute->setAttributeType(Qt3DCore::QAttribute::VertexAttribute);
132 m_positionAttribute->setBuffer(m_vertexBuffer);
133 m_positionAttribute->setByteStride(stride);
134 m_positionAttribute->setByteOffset(0);
135 m_positionAttribute->setCount(0);
136
137 m_normalAttribute->setName(Qt3DCore::QAttribute::defaultNormalAttributeName());
138 m_normalAttribute->setVertexBaseType(Qt3DCore::QAttribute::Float);
139 m_normalAttribute->setVertexSize(3);
140 m_normalAttribute->setAttributeType(Qt3DCore::QAttribute::VertexAttribute);
141 m_normalAttribute->setBuffer(m_vertexBuffer);
142 m_normalAttribute->setByteStride(stride);
143 m_normalAttribute->setByteOffset(3 * sizeof(float));
144 m_normalAttribute->setCount(0);
145
146 m_indexAttribute->setAttributeType(Qt3DCore::QAttribute::IndexAttribute);
147 m_indexAttribute->setVertexBaseType(Qt3DCore::QAttribute::UnsignedInt);
148 m_indexAttribute->setBuffer(m_indexBuffer);
149 m_indexAttribute->setCount(0);
150
151 q->addAttribute(attribute: m_positionAttribute);
152 q->addAttribute(attribute: m_normalAttribute);
153 q->addAttribute(attribute: m_indexAttribute);
154
155 update();
156}
157
158/*!
159 * \qmltype ExtrudedTextGeometry
160 * \instantiates Qt3DExtras::QExtrudedTextGeometry
161 * \inqmlmodule Qt3D.Extras
162 * \brief ExtrudedTextGeometry allows creation of a 3D text in 3D space.
163 *
164 * The ExtrudedTextGeometry type is most commonly used internally by the
165 * ExtrudedTextMesh type but can also be used in custom GeometryRenderer types.
166 *
167 * The origin of the geometry is the rear left end of the text's baseline.
168 */
169
170/*!
171 * \qmlproperty QString ExtrudedTextGeometry::text
172 *
173 * Holds the text used for the mesh.
174 */
175
176/*!
177 * \qmlproperty QFont ExtrudedTextGeometry::font
178 *
179 * Holds the font of the text.
180 *
181 * The geometry is normalized by the font's pointSize, so a larger pointSize
182 * will result in smoother, rather than larger, text. pixelSize should not
183 * be used.
184 */
185
186/*!
187 * \qmlproperty float ExtrudedTextGeometry::depth
188 *
189 * Holds the extrusion depth of the text.
190 */
191
192/*!
193 * \qmlproperty Attribute ExtrudedTextGeometry::positionAttribute
194 *
195 * Holds the geometry position attribute.
196 */
197
198/*!
199 * \qmlproperty Attribute ExtrudedTextGeometry::normalAttribute
200 *
201 * Holds the geometry normal attribute.
202 */
203
204/*!
205 * \qmlproperty Attribute ExtrudedTextGeometry::indexAttribute
206 *
207 * Holds the geometry index attribute.
208 */
209
210/*!
211 * \class Qt3DExtras::QExtrudedTextGeometry
212 * \inheaderfile Qt3DExtras/QExtrudedTextGeometry
213 * \inmodule Qt3DExtras
214 * \brief The QExtrudedTextGeometry class allows creation of a 3D extruded text
215 * in 3D space.
216 * \since 5.9
217 * \ingroup geometries
218 * \inherits Qt3DCore::QGeometry
219 *
220 * The QExtrudedTextGeometry class is most commonly used internally by the QText3DMesh
221 * but can also be used in custom Qt3DRender::QGeometryRenderer subclasses.
222 *
223 * The origin of the geometry is the rear left end of the text's baseline.
224 */
225
226/*!
227 * Constructs a new QExtrudedTextGeometry with \a parent.
228 */
229QExtrudedTextGeometry::QExtrudedTextGeometry(Qt3DCore::QNode *parent)
230 : QGeometry(*new QExtrudedTextGeometryPrivate(), parent)
231{
232 Q_D(QExtrudedTextGeometry);
233 d->init();
234}
235
236/*!
237 * \internal
238 */
239QExtrudedTextGeometry::QExtrudedTextGeometry(QExtrudedTextGeometryPrivate &dd, Qt3DCore::QNode *parent)
240 : QGeometry(dd, parent)
241{
242 Q_D(QExtrudedTextGeometry);
243 d->init();
244}
245
246/*!
247 * \internal
248 */
249QExtrudedTextGeometry::~QExtrudedTextGeometry()
250{}
251
252/*!
253 * \internal
254 * Updates vertices based on text, font, extrusionLength and smoothAngle properties.
255 */
256void QExtrudedTextGeometryPrivate::update()
257{
258 if (m_text.trimmed().isEmpty()) // save enough?
259 return;
260
261 TriangulationData data = triangulate(text: m_text, font: m_font);
262
263 const IndexType numVertices = IndexType(data.vertices.size());
264 const size_t numIndices = data.indices.size();
265
266 struct Vertex {
267 QVector3D position;
268 QVector3D normal;
269 };
270
271 std::vector<IndexType> indices;
272 std::vector<Vertex> vertices;
273
274 // TODO: keep 'vertices.size()' small when extruding
275 vertices.reserve(n: data.vertices.size() * 2);
276 for (QVector3D &v : data.vertices) // front face
277 vertices.push_back(x: { .position: v, // vertex
278 .normal: QVector3D(0.0f, 0.0f, -1.0f) }); // normal
279 for (QVector3D &v : data.vertices) // front face
280 vertices.push_back(x: { .position: QVector3D(v.x(), v.y(), m_depth), // vertex
281 .normal: QVector3D(0.0f, 0.0f, 1.0f) }); // normal
282
283 int verticesIndex = int(vertices.size());
284 for (size_t i = 0; i < data.outlines.size(); ++i) {
285 const int begin = data.outlines[i].begin;
286 const int end = data.outlines[i].end;
287 const int verticesIndexBegin = verticesIndex;
288
289 if (begin == end)
290 continue;
291
292 QVector3D prevNormal = QVector3D::crossProduct(
293 v1: vertices[data.outlineIndices[end - 1] + numVertices].position - vertices[data.outlineIndices[end - 1]].position,
294 v2: vertices[data.outlineIndices[begin]].position - vertices[data.outlineIndices[end - 1]].position).normalized();
295
296 for (int j = begin; j < end; ++j) {
297 const bool isLastIndex = (j == end - 1);
298 const IndexType cur = data.outlineIndices[j];
299 const IndexType next = data.outlineIndices[((j - begin + 1) % (end - begin)) + begin]; // normalize, bring in range and adjust
300 const QVector3D normal = QVector3D::crossProduct(v1: vertices[cur + numVertices].position - vertices[cur].position, v2: vertices[next].position - vertices[cur].position).normalized();
301
302 // use smooth normals in case of a short angle
303 const bool smooth = QVector3D::dotProduct(v1: prevNormal, v2: normal) > (90.0f - edgeSplitAngle) / 90.0f;
304 const QVector3D resultNormal = smooth ? mix(a: prevNormal, b: normal, ratio: 0.5f) : normal;
305 if (!smooth) {
306 vertices.push_back(x: {.position: vertices[cur].position, .normal: prevNormal});
307 vertices.push_back(x: {.position: vertices[cur + numVertices].position, .normal: prevNormal});
308 verticesIndex += 2;
309 }
310
311 vertices.push_back(x: {.position: vertices[cur].position, .normal: resultNormal});
312 vertices.push_back(x: {.position: vertices[cur + numVertices].position, .normal: resultNormal});
313
314 const int v0 = verticesIndex;
315 const int v1 = verticesIndex + 1;
316 const int v2 = isLastIndex ? verticesIndexBegin : verticesIndex + 2;
317 const int v3 = isLastIndex ? verticesIndexBegin + 1 : verticesIndex + 3;
318
319 indices.push_back(x: v0);
320 indices.push_back(x: v1);
321 indices.push_back(x: v2);
322 indices.push_back(x: v2);
323 indices.push_back(x: v1);
324 indices.push_back(x: v3);
325
326 verticesIndex += 2;
327 prevNormal = normal;
328 }
329 }
330
331 { // upload vertices
332 QByteArray data;
333 data.resize(size: vertices.size() * sizeof(Vertex));
334 memcpy(dest: data.data(), src: vertices.data(), n: vertices.size() * sizeof(Vertex));
335
336 m_vertexBuffer->setData(data);
337 m_positionAttribute->setCount(int(vertices.size()));
338 m_normalAttribute->setCount(int(vertices.size()));
339 }
340
341 // resize for following insertions
342 const int indicesOffset = int(indices.size());
343 indices.resize(new_size: indices.size() + numIndices * 2);
344
345 // copy values for back faces
346 IndexType *indicesFaces = indices.data() + indicesOffset;
347 memcpy(dest: indicesFaces, src: data.indices.data(), n: numIndices * sizeof(IndexType));
348
349 // insert values for front face and flip triangles
350 for (size_t j = 0; j < numIndices; j += 3) {
351 indicesFaces[numIndices + j ] = indicesFaces[j ] + numVertices;
352 indicesFaces[numIndices + j + 1] = indicesFaces[j + 2] + numVertices;
353 indicesFaces[numIndices + j + 2] = indicesFaces[j + 1] + numVertices;
354 }
355
356 { // upload indices
357 QByteArray data;
358 data.resize(size: indices.size() * sizeof(IndexType));
359 memcpy(dest: data.data(), src: indices.data(), n: indices.size() * sizeof(IndexType));
360
361 m_indexBuffer->setData(data);
362 m_indexAttribute->setCount(uint(indices.size()));
363 }
364}
365
366void QExtrudedTextGeometry::setText(const QString &text)
367{
368 Q_D(QExtrudedTextGeometry);
369 if (d->m_text != text) {
370 d->m_text = text;
371 d->update();
372 emit textChanged(text);
373 }
374}
375
376void QExtrudedTextGeometry::setFont(const QFont &font)
377{
378 Q_D(QExtrudedTextGeometry);
379 if (d->m_font != font) {
380 d->m_font = font;
381 d->update();
382 emit fontChanged(font);
383 }
384}
385
386void QExtrudedTextGeometry::setDepth(float depth)
387{
388 Q_D(QExtrudedTextGeometry);
389 if (d->m_depth != depth) {
390 d->m_depth = depth;
391 d->update();
392 emit depthChanged(extrusionLength: depth);
393 }
394}
395
396/*!
397 * \property QExtrudedTextGeometry::text
398 *
399 * Holds the text used for the mesh.
400 */
401QString QExtrudedTextGeometry::text() const
402{
403 Q_D(const QExtrudedTextGeometry);
404 return d->m_text;
405}
406
407/*!
408 * \property QExtrudedTextGeometry::font
409 *
410 * Holds the font of the text.
411 *
412 * The geometry is normalized by the font's pointSize, so a larger pointSize
413 * will result in smoother, rather than larger, text. pixelSize should not
414 * be used.
415 */
416QFont QExtrudedTextGeometry::font() const
417{
418 Q_D(const QExtrudedTextGeometry);
419 return d->m_font;
420}
421
422/*!
423 * \property QExtrudedTextGeometry::extrusionLength
424 *
425 * Holds the extrusion length of the text.
426 */
427float QExtrudedTextGeometry::extrusionLength() const
428{
429 Q_D(const QExtrudedTextGeometry);
430 return d->m_depth;
431}
432
433/*!
434 * \property QExtrudedTextGeometry::positionAttribute
435 *
436 * Holds the geometry position attribute.
437 */
438Qt3DCore::QAttribute *QExtrudedTextGeometry::positionAttribute() const
439{
440 Q_D(const QExtrudedTextGeometry);
441 return d->m_positionAttribute;
442}
443
444/*!
445 * \property QExtrudedTextGeometry::normalAttribute
446 *
447 * Holds the geometry normal attribute.
448 */
449Qt3DCore::QAttribute *QExtrudedTextGeometry::normalAttribute() const
450{
451 Q_D(const QExtrudedTextGeometry);
452 return d->m_normalAttribute;
453}
454
455/*!
456 * \property QExtrudedTextGeometry::indexAttribute
457 *
458 * Holds the geometry index attribute.
459 */
460Qt3DCore::QAttribute *QExtrudedTextGeometry::indexAttribute() const
461{
462 Q_D(const QExtrudedTextGeometry);
463 return d->m_indexAttribute;
464}
465
466} // Qt3DExtras
467
468QT_END_NAMESPACE
469
470#include "moc_qextrudedtextgeometry.cpp"
471

source code of qt3d/src/extras/3dtext/qextrudedtextgeometry.cpp