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 | * \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 | |
102 | QHash<Qt3DCore::QScene *, QText2DEntityPrivate::CacheEntry> QText2DEntityPrivate::; |
103 | |
104 | 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 | |
114 | QText2DEntityPrivate::() |
115 | { |
116 | } |
117 | |
118 | void QText2DEntityPrivate::(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 | |
157 | QText2DEntity::(QNode *parent) |
158 | : Qt3DCore::QEntity(*new QText2DEntityPrivate(), parent) |
159 | { |
160 | } |
161 | |
162 | /*! \internal */ |
163 | QText2DEntity::() |
164 | { |
165 | } |
166 | |
167 | qreal QText2DEntityPrivate::() 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 | |
176 | struct { |
177 | int = 0; |
178 | std::vector<float> ; |
179 | std::vector<quint16> ; |
180 | }; |
181 | |
182 | void QText2DEntityPrivate::(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 | |
291 | void QText2DEntityPrivate::() |
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 | |
298 | void QText2DEntityPrivate::() |
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 | */ |
340 | QFont QText2DEntity::() const |
341 | { |
342 | Q_D(const QText2DEntity); |
343 | return d->m_font; |
344 | } |
345 | |
346 | void QText2DEntity::(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 | */ |
370 | QColor QText2DEntity::() const |
371 | { |
372 | Q_D(const QText2DEntity); |
373 | return d->m_color; |
374 | } |
375 | |
376 | void QText2DEntity::(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 | */ |
394 | QString QText2DEntity::() const |
395 | { |
396 | Q_D(const QText2DEntity); |
397 | return d->m_text; |
398 | } |
399 | |
400 | void QText2DEntity::(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 | */ |
417 | float QText2DEntity::() 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 | */ |
429 | float QText2DEntity::() const |
430 | { |
431 | Q_D(const QText2DEntity); |
432 | return d->m_height; |
433 | } |
434 | |
435 | void QText2DEntity::(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 | |
445 | void QText2DEntity::(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 | |
457 | QT_END_NAMESPACE |
458 | |
459 | #include "moc_qtext2dentity.cpp" |
460 | |