1// Copyright (C) 2016 Klaralvdalens Datakonsult AB (KDAB).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qtext2dentity.h"
5#include "qtext2dentity_p.h"
6#include "qtext2dmaterial_p.h"
7
8#include <QtGui/qtextlayout.h>
9#include <QtGui/qglyphrun.h>
10#include <QtGui/private/qdistancefield_p.h>
11#include <QtGui/private/qtextureglyphcache_p.h>
12#include <QtGui/private/qfont_p.h>
13#include <QtGui/private/qdistancefield_p.h>
14
15#include <Qt3DCore/qbuffer.h>
16#include <Qt3DCore/qattribute.h>
17#include <Qt3DCore/qgeometry.h>
18#include <Qt3DRender/qmaterial.h>
19#include <Qt3DRender/qgeometryrenderer.h>
20
21#include <Qt3DCore/private/qscene_p.h>
22
23QT_BEGIN_NAMESPACE
24
25namespace {
26
27inline constexpr QRectF scaleRectF(const QRectF &rect, qreal scale)
28{
29 return QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale);
30}
31
32} // anonymous
33
34namespace Qt3DExtras {
35
36/*!
37 * \qmltype Text2DEntity
38 * \instantiates Qt3DExtras::QText2DEntity
39 * \inqmlmodule Qt3D.Extras
40 * \brief Text2DEntity allows creation of a 2D text in 3D space.
41 *
42 * The Text2DEntity renders text as triangles in the XY plane. The geometry will be fitted
43 * in the rectangle of specified width and height. If the resulting geometry is wider than
44 * the specified width, the remainder will be rendered on the new line.
45 *
46 * The entity can be positionned in the scene by adding a transform component.
47 *
48 * Text2DEntity will create geometry based on the shape of the glyphs and a solid
49 * material using the specified color.
50 *
51 */
52
53/*!
54 * \qmlproperty QString Text2DEntity::text
55 *
56 * Holds the text used for the mesh.
57 */
58
59/*!
60 * \qmlproperty QFont Text2DEntity::font
61 *
62 * Holds the font of the text.
63 */
64
65/*!
66 * \qmlproperty QColor Text2DEntity::color
67 *
68 * Holds the color of the text.
69 */
70
71/*!
72 * \qmlproperty float Text2DEntity::width
73 *
74 * Holds the width of the text's bounding rectangle.
75 */
76
77/*!
78 * \qmlproperty float Text2DEntity::height
79 *
80 * Holds the height of the text's bounding rectangle.
81 */
82
83
84/*!
85 * \class Qt3DExtras::QText2DEntity
86 * \inheaderfile Qt3DExtras/QText2DEntity
87 * \inmodule Qt3DExtras
88 *
89 * \brief QText2DEntity allows creation of a 2D text in 3D space.
90 *
91 * The QText2DEntity renders text as triangles in the XY plane. The geometry will be fitted
92 * in the rectangle of specified width and height. If the resulting geometry is wider than
93 * the specified width, the remainder will be rendered on the new line.
94 *
95 * The entity can be positionned in the scene by adding a transform component.
96 *
97 * QText2DEntity will create geometry based on the shape of the glyphs and a solid
98 * material using the specified color.
99 *
100 */
101
102QHash<Qt3DCore::QScene *, QText2DEntityPrivate::CacheEntry> QText2DEntityPrivate::m_glyphCacheInstances;
103
104QText2DEntityPrivate::QText2DEntityPrivate()
105 : m_glyphCache(nullptr)
106 , m_font(QLatin1String("Times"), 10)
107 , m_scaledFont(QLatin1String("Times"), 10)
108 , m_color(QColor(255, 255, 255, 255))
109 , m_width(0.0f)
110 , m_height(0.0f)
111{
112}
113
114QText2DEntityPrivate::~QText2DEntityPrivate()
115{
116}
117
118void QText2DEntityPrivate::setScene(Qt3DCore::QScene *scene)
119{
120 if (scene == m_scene)
121 return;
122
123 // Unref old glyph cache if it exists
124 if (m_scene != nullptr) {
125 // Ensure we don't keep reference to glyphs
126 // if we are changing the cache
127 if (m_glyphCache != nullptr)
128 clearCurrentGlyphRuns();
129
130 m_glyphCache = nullptr;
131
132 QText2DEntityPrivate::CacheEntry &entry = QText2DEntityPrivate::m_glyphCacheInstances[m_scene];
133 --entry.count;
134 if (entry.count == 0 && entry.glyphCache != nullptr) {
135
136 delete entry.glyphCache;
137 entry.glyphCache = nullptr;
138 }
139 }
140
141 QEntityPrivate::setScene(scene);
142
143 // Ref new glyph cache is scene is valid
144 if (scene != nullptr) {
145 QText2DEntityPrivate::CacheEntry &entry = QText2DEntityPrivate::m_glyphCacheInstances[scene];
146 if (entry.glyphCache == nullptr) {
147 entry.glyphCache = new QDistanceFieldGlyphCache();
148 entry.glyphCache->setRootNode(scene->rootNode());
149 }
150 m_glyphCache = entry.glyphCache;
151 ++entry.count;
152 // Update to populate glyphCache if needed
153 updateGlyphs();
154 }
155}
156
157QText2DEntity::QText2DEntity(QNode *parent)
158 : Qt3DCore::QEntity(*new QText2DEntityPrivate(), parent)
159{
160}
161
162/*! \internal */
163QText2DEntity::~QText2DEntity()
164{
165}
166
167qreal QText2DEntityPrivate::computeActualScale() const
168{
169 // scale font based on fontScale property and given QFont
170 float scale = 1.0f;
171 if (m_font.pointSizeF() > 0)
172 scale *= m_font.pointSizeF() / m_scaledFont.pointSizeF();
173 return scale;
174}
175
176struct RenderData {
177 int vertexCount = 0;
178 std::vector<float> vertex;
179 std::vector<quint16> index;
180};
181
182void QText2DEntityPrivate::setCurrentGlyphRuns(const QList<QGlyphRun> &runs)
183{
184 // For each distinct texture, we need a separate DistanceFieldTextRenderer,
185 // for which we need vertex and index data
186 QHash<Qt3DRender::QAbstractTexture*, RenderData> renderData;
187 const qreal scale = computeActualScale();
188
189 // process glyph runs
190 for (const QGlyphRun &run : runs) {
191 const auto glyphs = run.glyphIndexes();
192 const auto pos = run.positions();
193
194 Q_ASSERT(glyphs.size() == pos.size());
195
196 const bool doubleGlyphResolution = m_glyphCache->doubleGlyphResolution(font: run.rawFont());
197
198 // faithfully copied from QSGDistanceFieldGlyphNode::updateGeometry()
199 const qreal pixelSize = run.rawFont().pixelSize();
200 const qreal fontScale = pixelSize / QT_DISTANCEFIELD_BASEFONTSIZE(narrowOutlineFont: doubleGlyphResolution);
201 const qreal margin = QT_DISTANCEFIELD_RADIUS(narrowOutlineFont: doubleGlyphResolution) / QT_DISTANCEFIELD_SCALE(narrowOutlineFont: doubleGlyphResolution) * fontScale;
202
203 for (int i = 0; i < glyphs.size(); i++) {
204 const QDistanceFieldGlyphCache::Glyph &dfield = m_glyphCache->refGlyph(font: run.rawFont(), glyph: glyphs[i]);
205
206 if (!dfield.texture)
207 continue;
208
209 RenderData &data = renderData[dfield.texture];
210
211 // faithfully copied from QSGDistanceFieldGlyphNode::updateGeometry()
212 QRectF metrics = scaleRectF(rect: dfield.glyphPathBoundingRect, scale: fontScale);
213 metrics.adjust(xp1: -margin, yp1: margin, xp2: margin, yp2: 3*margin);
214
215 const qreal top = 0.0;
216 const qreal left = 0.0;
217 const qreal right = qreal(m_width);
218 const qreal bottom = qreal(m_height);
219
220 qreal x1 = left + scale * (pos[i].x() + metrics.left());
221 qreal y2 = bottom - scale * (pos[i].y() - metrics.top());
222 qreal x2 = x1 + scale * metrics.width();
223 qreal y1 = y2 - scale * metrics.height();
224
225 // only draw glyphs that are at least partly visible
226 if (y2 < top || x1 > right)
227 continue;
228
229 QRectF texCoords = dfield.texCoords;
230
231 // if a glyph is only partly visible within the given rectangle,
232 // cut it in half and adjust tex coords
233 if (y1 < top) {
234 const auto insideRatio = (top - y2) / (y1 - y2);
235 y1 = top;
236 texCoords.setHeight(texCoords.height() * insideRatio);
237 }
238
239 // do the same thing horizontally
240 if (x2 > right) {
241 const auto insideRatio = (right - x1) / (x2 - x1);
242 x2 = right;
243 texCoords.setWidth(texCoords.width() * insideRatio);
244 }
245
246 for (auto v: std::vector<qreal>{x1, y1, qreal(i), texCoords.left(), texCoords.bottom()})
247 data.vertex.push_back(x: float(v));
248 for (auto v: std::vector<qreal>{x1, y2, qreal(i), texCoords.left(), texCoords.top()})
249 data.vertex.push_back(x: float(v));
250 for (auto v: std::vector<qreal>{x2, y1, qreal(i), texCoords.right(), texCoords.bottom()})
251 data.vertex.push_back(x: float(v));
252 for (auto v: std::vector<qreal>{x2, y2, qreal(i), texCoords.right(), texCoords.top()})
253 data.vertex.push_back(x: float(v));
254
255 for (int i: std::vector<int>{data.vertexCount, data.vertexCount + 3, data.vertexCount + 1})
256 data.index.push_back(x: quint16(i));
257 for (int i: std::vector<int>{data.vertexCount, data.vertexCount + 2, data.vertexCount + 3})
258 data.index.push_back(x: quint16(i));
259
260 data.vertexCount += 4;
261 }
262 }
263
264 // de-ref all glyphs for previous QGlyphRuns
265 for (int i = 0; i < m_currentGlyphRuns.size(); i++)
266 m_glyphCache->derefGlyphs(run: m_currentGlyphRuns[i]);
267 m_currentGlyphRuns = runs;
268
269 // make sure we have the correct number of DistanceFieldTextRenderers
270 // TODO: we might keep one renderer at all times, so we won't delete and
271 // re-allocate one every time the text changes from an empty to a non-empty string
272 // and vice-versa
273 while (m_renderers.size() > renderData.size())
274 delete m_renderers.takeLast();
275
276 while (m_renderers.size() < renderData.size()) {
277 DistanceFieldTextRenderer *renderer = new DistanceFieldTextRenderer();
278 renderer->setColor(m_color);
279 renderer->setParent(q_func());
280 m_renderers << renderer;
281 }
282
283 Q_ASSERT(m_renderers.size() == renderData.size());
284
285 // assign vertex data for all textures to the renderers
286 int rendererIdx = 0;
287 for (auto it = renderData.begin(); it != renderData.end(); ++it)
288 m_renderers[rendererIdx++]->setGlyphData(glyphTexture: it.key(), vertexData: it.value().vertex, indexData: it.value().index);
289}
290
291void QText2DEntityPrivate::clearCurrentGlyphRuns()
292{
293 for (int i = 0; i < m_currentGlyphRuns.size(); i++)
294 m_glyphCache->derefGlyphs(run: m_currentGlyphRuns[i]);
295 m_currentGlyphRuns.clear();
296}
297
298void QText2DEntityPrivate::updateGlyphs()
299{
300 if (m_glyphCache == nullptr)
301 return;
302
303 QList<QGlyphRun> glyphRuns;
304
305 // collect all GlyphRuns generated by the QTextLayout
306 if ((m_width > 0.0f || m_height > 0.0f) && !m_text.isEmpty()) {
307 QTextLayout layout(m_text, m_scaledFont);
308 const float lineWidth = m_width / computeActualScale();
309 float height = 0;
310 layout.beginLayout();
311
312 while (true) {
313 QTextLine line = layout.createLine();
314 if (!line.isValid())
315 break;
316
317 // position current line
318 line.setLineWidth(lineWidth);
319 line.setPosition(QPointF(0, height));
320 height += line.height();
321
322 // add glyph runs created by line
323 const QList<QGlyphRun> runs = line.glyphRuns();
324 for (const QGlyphRun &run : runs)
325 glyphRuns << run;
326 }
327
328 layout.endLayout();
329 }
330
331 setCurrentGlyphRuns(glyphRuns);
332}
333
334/*!
335 \property QText2DEntity::font
336
337 Holds the font for the text item that is displayed
338 in the Qt Quick scene.
339*/
340QFont QText2DEntity::font() const
341{
342 Q_D(const QText2DEntity);
343 return d->m_font;
344}
345
346void QText2DEntity::setFont(const QFont &font)
347{
348 Q_D(QText2DEntity);
349 if (d->m_font != font) {
350 // ignore the point size of the font, just make it a default value.
351 // still we want to make sure that font() returns the same value
352 // that was passed to setFont(), so we store it nevertheless
353 d->m_font = font;
354 d->m_scaledFont = font;
355 d->m_scaledFont.setPointSize(10);
356
357 emit fontChanged(font);
358
359 if (!d->m_text.isEmpty())
360 d->updateGlyphs();
361 }
362}
363
364/*!
365 \property QText2DEntity::color
366
367 Holds the color for the text item that is displayed in the Qt
368 Quick scene.
369*/
370QColor QText2DEntity::color() const
371{
372 Q_D(const QText2DEntity);
373 return d->m_color;
374}
375
376void QText2DEntity::setColor(const QColor &color)
377{
378 Q_D(QText2DEntity);
379 if (d->m_color != color) {
380 d->m_color = color;
381
382 emit colorChanged(color);
383
384 for (DistanceFieldTextRenderer *renderer : std::as_const(t&: d->m_renderers))
385 renderer->setColor(color);
386 }
387}
388
389/*!
390 \property QText2DEntity::text
391
392 Holds the text that is displayed in the Qt Quick scene.
393*/
394QString QText2DEntity::text() const
395{
396 Q_D(const QText2DEntity);
397 return d->m_text;
398}
399
400void QText2DEntity::setText(const QString &text)
401{
402 Q_D(QText2DEntity);
403 if (d->m_text != text) {
404 d->m_text = text;
405 emit textChanged(text);
406
407 d->updateGlyphs();
408 }
409}
410
411/*!
412 \property QText2DEntity::width
413
414 Returns the width of the text item that is displayed in the
415 Qt Quick scene.
416*/
417float QText2DEntity::width() const
418{
419 Q_D(const QText2DEntity);
420 return d->m_width;
421}
422
423/*!
424 \property QText2DEntity::height
425
426 Returns the height of the text item that is displayed in the
427 Qt Quick scene.
428*/
429float QText2DEntity::height() const
430{
431 Q_D(const QText2DEntity);
432 return d->m_height;
433}
434
435void QText2DEntity::setWidth(float width)
436{
437 Q_D(QText2DEntity);
438 if (width != d->m_width) {
439 d->m_width = width;
440 emit widthChanged(width);
441 d->updateGlyphs();
442 }
443}
444
445void QText2DEntity::setHeight(float height)
446{
447 Q_D(QText2DEntity);
448 if (height != d->m_height) {
449 d->m_height = height;
450 emit heightChanged(height);
451 d->updateGlyphs();
452 }
453}
454
455} // namespace Qt3DExtras
456
457QT_END_NAMESPACE
458
459#include "moc_qtext2dentity.cpp"
460

source code of qt3d/src/extras/text/qtext2dentity.cpp