1/****************************************************************************
2**
3** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt3D module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:BSD$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** BSD License Usage
18** Alternatively, you may use this file under the terms of the BSD license
19** as follows:
20**
21** "Redistribution and use in source and binary forms, with or without
22** modification, are permitted provided that the following conditions are
23** met:
24** * Redistributions of source code must retain the above copyright
25** notice, this list of conditions and the following disclaimer.
26** * Redistributions in binary form must reproduce the above copyright
27** notice, this list of conditions and the following disclaimer in
28** the documentation and/or other materials provided with the
29** distribution.
30** * Neither the name of The Qt Company Ltd nor the names of its
31** contributors may be used to endorse or promote products derived
32** from this software without specific prior written permission.
33**
34**
35** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46**
47** $QT_END_LICENSE$
48**
49****************************************************************************/
50
51#include "qextrudedtextgeometry.h"
52#include "qextrudedtextgeometry_p.h"
53#include <Qt3DRender/qbuffer.h>
54#include <Qt3DRender/qbufferdatagenerator.h>
55#include <Qt3DRender/qattribute.h>
56#include <private/qtriangulator_p.h>
57#include <qmath.h>
58#include <QVector3D>
59#include <QTextLayout>
60#include <QTime>
61#include <QPainterPath>
62
63QT_BEGIN_NAMESPACE
64
65namespace Qt3DExtras {
66
67namespace {
68
69static float edgeSplitAngle = 90.f * 0.1f;
70
71using IndexType = unsigned int;
72
73struct TriangulationData {
74 struct Outline {
75 int begin;
76 int end;
77 };
78
79 QVector<QVector3D> vertices;
80 QVector<IndexType> indices;
81 QVector<Outline> outlines;
82 QVector<IndexType> outlineIndices;
83 bool inverted;
84};
85
86TriangulationData triangulate(const QString &text, const QFont &font)
87{
88 TriangulationData result;
89 int beginOutline = 0;
90
91 // Initialize path with text and extract polygons
92 QPainterPath path;
93 path.setFillRule(Qt::WindingFill);
94 path.addText(x: 0, y: 0, f: font, text);
95 QList<QPolygonF> polygons = path.toSubpathPolygons(matrix: QTransform().scale(sx: 1.f, sy: -1.f));
96
97 // maybe glyph has no geometry
98 if (polygons.size() == 0)
99 return result;
100
101 const int prevNumIndices = result.indices.size();
102
103 // Reset path and add previously extracted polygons (which where spatially transformed)
104 path = QPainterPath();
105 path.setFillRule(Qt::WindingFill);
106 for (QPolygonF &p : polygons)
107 path.addPolygon(polygon: p);
108
109 // Extract polylines out of the path, this allows us to retrieve indices for each glyph outline
110 QPolylineSet polylines = qPolyline(path);
111 QVector<IndexType> tmpIndices;
112 tmpIndices.resize(asize: polylines.indices.size());
113 memcpy(dest: tmpIndices.data(), src: polylines.indices.data(), n: polylines.indices.size() * sizeof(IndexType));
114
115 int lastIndex = 0;
116 for (const IndexType idx : tmpIndices) {
117 if (idx == std::numeric_limits<IndexType>::max()) {
118 const int endOutline = lastIndex;
119 result.outlines.push_back(t: {.begin: beginOutline, .end: endOutline});
120 beginOutline = endOutline;
121 } else {
122 result.outlineIndices.push_back(t: idx);
123 ++lastIndex;
124 }
125 }
126
127 // Triangulate path
128 const QTriangleSet triangles = qTriangulate(path);
129
130 // Append new indices to result.indices buffer
131 result.indices.resize(asize: result.indices.size() + triangles.indices.size());
132 memcpy(dest: &result.indices[prevNumIndices], src: triangles.indices.data(), n: triangles.indices.size() * sizeof(IndexType));
133 for (int i = prevNumIndices, m = result.indices.size(); i < m; ++i)
134 result.indices[i] += result.vertices.size();
135
136 // Append new triangles to result.vertices
137 result.vertices.reserve(asize: triangles.vertices.size() / 2);
138 for (int i = 0, m = triangles.vertices.size(); i < m; i += 2)
139 result.vertices.push_back(t: QVector3D(triangles.vertices[i] / font.pointSizeF(), triangles.vertices[i + 1] / font.pointSizeF(), 0.0f));
140
141 return result;
142}
143
144inline QVector3D mix(const QVector3D &a, const QVector3D &b, float ratio)
145{
146 return a + (b - a) * ratio;
147}
148
149} // anonymous namespace
150
151QExtrudedTextGeometryPrivate::QExtrudedTextGeometryPrivate()
152 : QGeometryPrivate()
153 , m_font(QFont(QStringLiteral("Arial")))
154 , m_depth(1.f)
155 , m_positionAttribute(nullptr)
156 , m_normalAttribute(nullptr)
157 , m_indexAttribute(nullptr)
158 , m_vertexBuffer(nullptr)
159 , m_indexBuffer(nullptr)
160{
161 m_font.setPointSize(4);
162}
163
164void QExtrudedTextGeometryPrivate::init()
165{
166 Q_Q(QExtrudedTextGeometry);
167 m_positionAttribute = new Qt3DRender::QAttribute(q);
168 m_normalAttribute = new Qt3DRender::QAttribute(q);
169 m_indexAttribute = new Qt3DRender::QAttribute(q);
170 m_vertexBuffer = new Qt3DRender::QBuffer(q);
171 m_indexBuffer = new Qt3DRender::QBuffer(q);
172
173 const quint32 elementSize = 3 + 3;
174 const quint32 stride = elementSize * sizeof(float);
175
176 m_positionAttribute->setName(Qt3DRender::QAttribute::defaultPositionAttributeName());
177 m_positionAttribute->setVertexBaseType(Qt3DRender::QAttribute::Float);
178 m_positionAttribute->setVertexSize(3);
179 m_positionAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute);
180 m_positionAttribute->setBuffer(m_vertexBuffer);
181 m_positionAttribute->setByteStride(stride);
182 m_positionAttribute->setByteOffset(0);
183 m_positionAttribute->setCount(0);
184
185 m_normalAttribute->setName(Qt3DRender::QAttribute::defaultNormalAttributeName());
186 m_normalAttribute->setVertexBaseType(Qt3DRender::QAttribute::Float);
187 m_normalAttribute->setVertexSize(3);
188 m_normalAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute);
189 m_normalAttribute->setBuffer(m_vertexBuffer);
190 m_normalAttribute->setByteStride(stride);
191 m_normalAttribute->setByteOffset(3 * sizeof(float));
192 m_normalAttribute->setCount(0);
193
194 m_indexAttribute->setAttributeType(Qt3DRender::QAttribute::IndexAttribute);
195 m_indexAttribute->setVertexBaseType(Qt3DRender::QAttribute::UnsignedInt);
196 m_indexAttribute->setBuffer(m_indexBuffer);
197 m_indexAttribute->setCount(0);
198
199 q->addAttribute(attribute: m_positionAttribute);
200 q->addAttribute(attribute: m_normalAttribute);
201 q->addAttribute(attribute: m_indexAttribute);
202
203 update();
204}
205
206/*!
207 * \qmltype ExtrudedTextGeometry
208 * \instantiates Qt3DExtras::QExtrudedTextGeometry
209 * \inqmlmodule Qt3D.Extras
210 * \brief ExtrudedTextGeometry allows creation of a 3D text in 3D space.
211 *
212 * The ExtrudedTextGeometry type is most commonly used internally by the
213 * ExtrudedTextMesh type but can also be used in custom GeometryRenderer types.
214 *
215 * The origin of the geometry is the rear left end of the text's baseline.
216 */
217
218/*!
219 * \qmlproperty QString ExtrudedTextGeometry::text
220 *
221 * Holds the text used for the mesh.
222 */
223
224/*!
225 * \qmlproperty QFont ExtrudedTextGeometry::font
226 *
227 * Holds the font of the text.
228 *
229 * The geometry is normalized by the font's pointSize, so a larger pointSize
230 * will result in smoother, rather than larger, text. pixelSize should not
231 * be used.
232 */
233
234/*!
235 * \qmlproperty float ExtrudedTextGeometry::depth
236 *
237 * Holds the extrusion depth of the text.
238 */
239
240/*!
241 * \qmlproperty Attribute ExtrudedTextGeometry::positionAttribute
242 *
243 * Holds the geometry position attribute.
244 */
245
246/*!
247 * \qmlproperty Attribute ExtrudedTextGeometry::normalAttribute
248 *
249 * Holds the geometry normal attribute.
250 */
251
252/*!
253 * \qmlproperty Attribute ExtrudedTextGeometry::indexAttribute
254 *
255 * Holds the geometry index attribute.
256 */
257
258/*!
259 * \class Qt3DExtras::QExtrudedTextGeometry
260 * \inheaderfile Qt3DExtras/QExtrudedTextGeometry
261 * \inmodule Qt3DExtras
262 * \brief The QExtrudedTextGeometry class allows creation of a 3D extruded text
263 * in 3D space.
264 * \since 5.9
265 * \ingroup geometries
266 * \inherits Qt3DRender::QGeometry
267 *
268 * The QExtrudedTextGeometry class is most commonly used internally by the QText3DMesh
269 * but can also be used in custom Qt3DRender::QGeometryRenderer subclasses.
270 *
271 * The origin of the geometry is the rear left end of the text's baseline.
272 */
273
274/*!
275 * Constructs a new QExtrudedTextGeometry with \a parent.
276 */
277QExtrudedTextGeometry::QExtrudedTextGeometry(Qt3DCore::QNode *parent)
278 : QGeometry(*new QExtrudedTextGeometryPrivate(), parent)
279{
280 Q_D(QExtrudedTextGeometry);
281 d->init();
282}
283
284/*!
285 * \internal
286 */
287QExtrudedTextGeometry::QExtrudedTextGeometry(QExtrudedTextGeometryPrivate &dd, Qt3DCore::QNode *parent)
288 : QGeometry(dd, parent)
289{
290 Q_D(QExtrudedTextGeometry);
291 d->init();
292}
293
294/*!
295 * \internal
296 */
297QExtrudedTextGeometry::~QExtrudedTextGeometry()
298{}
299
300/*!
301 * \internal
302 * Updates vertices based on text, font, extrusionLength and smoothAngle properties.
303 */
304void QExtrudedTextGeometryPrivate::update()
305{
306 if (m_text.trimmed().isEmpty()) // save enough?
307 return;
308
309 TriangulationData data = triangulate(text: m_text, font: m_font);
310
311 const int numVertices = data.vertices.size();
312 const int numIndices = data.indices.size();
313
314 struct Vertex {
315 QVector3D position;
316 QVector3D normal;
317 };
318
319 QVector<IndexType> indices;
320 QVector<Vertex> vertices;
321
322 // TODO: keep 'vertices.size()' small when extruding
323 vertices.reserve(asize: data.vertices.size() * 2);
324 for (QVector3D &v : data.vertices) // front face
325 vertices.push_back(t: { .position: v, // vertex
326 .normal: QVector3D(0.0f, 0.0f, -1.0f) }); // normal
327 for (QVector3D &v : data.vertices) // front face
328 vertices.push_back(t: { .position: QVector3D(v.x(), v.y(), m_depth), // vertex
329 .normal: QVector3D(0.0f, 0.0f, 1.0f) }); // normal
330
331 for (int i = 0, verticesIndex = vertices.size(); i < data.outlines.size(); ++i) {
332 const int begin = data.outlines[i].begin;
333 const int end = data.outlines[i].end;
334 const int verticesIndexBegin = verticesIndex;
335
336 if (begin == end)
337 continue;
338
339 QVector3D prevNormal = QVector3D::crossProduct(
340 v1: vertices[data.outlineIndices[end - 1] + numVertices].position - vertices[data.outlineIndices[end - 1]].position,
341 v2: vertices[data.outlineIndices[begin]].position - vertices[data.outlineIndices[end - 1]].position).normalized();
342
343 for (int j = begin; j < end; ++j) {
344 const bool isLastIndex = (j == end - 1);
345 const IndexType cur = data.outlineIndices[j];
346 const IndexType next = data.outlineIndices[((j - begin + 1) % (end - begin)) + begin]; // normalize, bring in range and adjust
347 const QVector3D normal = QVector3D::crossProduct(v1: vertices[cur + numVertices].position - vertices[cur].position, v2: vertices[next].position - vertices[cur].position).normalized();
348
349 // use smooth normals in case of a short angle
350 const bool smooth = QVector3D::dotProduct(v1: prevNormal, v2: normal) > (90.0f - edgeSplitAngle) / 90.0f;
351 const QVector3D resultNormal = smooth ? mix(a: prevNormal, b: normal, ratio: 0.5f) : normal;
352 if (!smooth) {
353 vertices.push_back(t: {.position: vertices[cur].position, .normal: prevNormal});
354 vertices.push_back(t: {.position: vertices[cur + numVertices].position, .normal: prevNormal});
355 verticesIndex += 2;
356 }
357
358 vertices.push_back(t: {.position: vertices[cur].position, .normal: resultNormal});
359 vertices.push_back(t: {.position: vertices[cur + numVertices].position, .normal: resultNormal});
360
361 const int v0 = verticesIndex;
362 const int v1 = verticesIndex + 1;
363 const int v2 = isLastIndex ? verticesIndexBegin : verticesIndex + 2;
364 const int v3 = isLastIndex ? verticesIndexBegin + 1 : verticesIndex + 3;
365
366 indices.push_back(t: v0);
367 indices.push_back(t: v1);
368 indices.push_back(t: v2);
369 indices.push_back(t: v2);
370 indices.push_back(t: v1);
371 indices.push_back(t: v3);
372
373 verticesIndex += 2;
374 prevNormal = normal;
375 }
376 }
377
378 { // upload vertices
379 QByteArray data;
380 data.resize(size: vertices.size() * sizeof(Vertex));
381 memcpy(dest: data.data(), src: vertices.data(), n: vertices.size() * sizeof(Vertex));
382
383 m_vertexBuffer->setData(data);
384 m_positionAttribute->setCount(vertices.size());
385 m_normalAttribute->setCount(vertices.size());
386 }
387
388 // resize for following insertions
389 const int indicesOffset = indices.size();
390 indices.resize(asize: indices.size() + numIndices * 2);
391
392 // copy values for back faces
393 IndexType *indicesFaces = indices.data() + indicesOffset;
394 memcpy(dest: indicesFaces, src: data.indices.data(), n: numIndices * sizeof(IndexType));
395
396 // insert values for front face and flip triangles
397 for (int j = 0; j < numIndices; j += 3)
398 {
399 indicesFaces[numIndices + j ] = indicesFaces[j ] + numVertices;
400 indicesFaces[numIndices + j + 1] = indicesFaces[j + 2] + numVertices;
401 indicesFaces[numIndices + j + 2] = indicesFaces[j + 1] + numVertices;
402 }
403
404 { // upload indices
405 QByteArray data;
406 data.resize(size: indices.size() * sizeof(IndexType));
407 memcpy(dest: data.data(), src: indices.data(), n: indices.size() * sizeof(IndexType));
408
409 m_indexBuffer->setData(data);
410 m_indexAttribute->setCount(indices.size());
411 }
412}
413
414void QExtrudedTextGeometry::setText(const QString &text)
415{
416 Q_D(QExtrudedTextGeometry);
417 if (d->m_text != text) {
418 d->m_text = text;
419 d->update();
420 emit textChanged(text);
421 }
422}
423
424void QExtrudedTextGeometry::setFont(const QFont &font)
425{
426 Q_D(QExtrudedTextGeometry);
427 if (d->m_font != font) {
428 d->m_font = font;
429 d->update();
430 emit fontChanged(font);
431 }
432}
433
434void QExtrudedTextGeometry::setDepth(float depth)
435{
436 Q_D(QExtrudedTextGeometry);
437 if (d->m_depth != depth) {
438 d->m_depth = depth;
439 d->update();
440 emit depthChanged(extrusionLength: depth);
441 }
442}
443
444/*!
445 * \property QExtrudedTextGeometry::text
446 *
447 * Holds the text used for the mesh.
448 */
449QString QExtrudedTextGeometry::text() const
450{
451 Q_D(const QExtrudedTextGeometry);
452 return d->m_text;
453}
454
455/*!
456 * \property QExtrudedTextGeometry::font
457 *
458 * Holds the font of the text.
459 *
460 * The geometry is normalized by the font's pointSize, so a larger pointSize
461 * will result in smoother, rather than larger, text. pixelSize should not
462 * be used.
463 */
464QFont QExtrudedTextGeometry::font() const
465{
466 Q_D(const QExtrudedTextGeometry);
467 return d->m_font;
468}
469
470/*!
471 * \property QExtrudedTextGeometry::extrusionLength
472 *
473 * Holds the extrusion length of the text.
474 */
475float QExtrudedTextGeometry::extrusionLength() const
476{
477 Q_D(const QExtrudedTextGeometry);
478 return d->m_depth;
479}
480
481/*!
482 * \property QExtrudedTextGeometry::positionAttribute
483 *
484 * Holds the geometry position attribute.
485 */
486Qt3DRender::QAttribute *QExtrudedTextGeometry::positionAttribute() const
487{
488 Q_D(const QExtrudedTextGeometry);
489 return d->m_positionAttribute;
490}
491
492/*!
493 * \property QExtrudedTextGeometry::normalAttribute
494 *
495 * Holds the geometry normal attribute.
496 */
497Qt3DRender::QAttribute *QExtrudedTextGeometry::normalAttribute() const
498{
499 Q_D(const QExtrudedTextGeometry);
500 return d->m_normalAttribute;
501}
502
503/*!
504 * \property QExtrudedTextGeometry::indexAttribute
505 *
506 * Holds the geometry index attribute.
507 */
508Qt3DRender::QAttribute *QExtrudedTextGeometry::indexAttribute() const
509{
510 Q_D(const QExtrudedTextGeometry);
511 return d->m_indexAttribute;
512}
513
514} // Qt3DExtras
515
516QT_END_NAMESPACE
517

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