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