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