// Copyright (C) 2016 Klaralvdalens Datakonsult AB (KDAB). // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qtext2dentity.h" #include "qtext2dentity_p.h" #include "qtext2dmaterial_p.h" #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace { inline constexpr QRectF scaleRectF(const QRectF &rect, qreal scale) { return QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale); } } // anonymous namespace Qt3DExtras { /*! * \qmltype Text2DEntity * \nativetype Qt3DExtras::QText2DEntity * \inqmlmodule Qt3D.Extras * \brief Text2DEntity allows creation of a 2D text in 3D space. * * The Text2DEntity renders text as triangles in the XY plane. The geometry will be fitted * in the rectangle of specified width and height. If the resulting geometry is wider than * the specified width, the remainder will be rendered on the new line. * * The entity can be positionned in the scene by adding a transform component. * * Text2DEntity will create geometry based on the shape of the glyphs and a solid * material using the specified color. * */ /*! * \qmlproperty string Text2DEntity::text * * Holds the text used for the mesh. */ /*! * \qmlproperty font Text2DEntity::font * * Holds the font of the text. */ /*! * \qmlproperty color Text2DEntity::color * * Holds the color of the text. */ /*! * \qmlproperty real Text2DEntity::width * * Holds the width of the text's bounding rectangle. */ /*! * \qmlproperty real Text2DEntity::height * * Holds the height of the text's bounding rectangle. */ /*! * \qmlproperty Qt::Alignment Text2DEntity::alignment * * Holds the alignment of the text. */ /*! * \class Qt3DExtras::QText2DEntity * \inheaderfile Qt3DExtras/QText2DEntity * \inmodule Qt3DExtras * * \brief QText2DEntity allows creation of a 2D text in 3D space. * * The QText2DEntity renders text as triangles in the XY plane. The geometry will be fitted * in the rectangle of specified width and height. If the resulting geometry is wider than * the specified width, the remainder will be rendered on the new line. * * The entity can be positionned in the scene by adding a transform component. * * QText2DEntity will create geometry based on the shape of the glyphs and a solid * material using the specified color. * */ QHash QText2DEntityPrivate::m_glyphCacheInstances; QText2DEntityPrivate::QText2DEntityPrivate() : m_glyphCache(nullptr) , m_font(QLatin1String("Times"), 10) , m_scaledFont(QLatin1String("Times"), 10) , m_color(QColor(255, 255, 255, 255)) , m_width(0.0f) , m_height(0.0f) , m_alignment(Qt::AlignTop | Qt::AlignLeft) { } QText2DEntityPrivate::~QText2DEntityPrivate() { } void QText2DEntityPrivate::setScene(Qt3DCore::QScene *scene) { if (scene == m_scene) return; // Unref old glyph cache if it exists if (m_scene != nullptr) { // Ensure we don't keep reference to glyphs // if we are changing the cache if (m_glyphCache != nullptr) clearCurrentGlyphRuns(); m_glyphCache = nullptr; QText2DEntityPrivate::CacheEntry &entry = QText2DEntityPrivate::m_glyphCacheInstances[m_scene]; --entry.count; if (entry.count == 0 && entry.glyphCache != nullptr) { delete entry.glyphCache; entry.glyphCache = nullptr; } } QEntityPrivate::setScene(scene); // Ref new glyph cache is scene is valid if (scene != nullptr) { QText2DEntityPrivate::CacheEntry &entry = QText2DEntityPrivate::m_glyphCacheInstances[scene]; if (entry.glyphCache == nullptr) { entry.glyphCache = new QDistanceFieldGlyphCache(); entry.glyphCache->setRootNode(scene->rootNode()); } m_glyphCache = entry.glyphCache; ++entry.count; // Update to populate glyphCache if needed updateGlyphs(); } } QText2DEntity::QText2DEntity(QNode *parent) : Qt3DCore::QEntity(*new QText2DEntityPrivate(), parent) { } /*! \internal */ QText2DEntity::~QText2DEntity() { } qreal QText2DEntityPrivate::computeActualScale() const { // scale font based on fontScale property and given QFont float scale = 1.0f; if (m_font.pointSizeF() > 0) scale *= m_font.pointSizeF() / m_scaledFont.pointSizeF(); return scale; } struct RenderData { int vertexCount = 0; std::vector vertex; std::vector index; }; void QText2DEntityPrivate::setCurrentGlyphRuns(const QList &runs) { // For each distinct texture, we need a separate DistanceFieldTextRenderer, // for which we need vertex and index data QHash renderData; const qreal scale = computeActualScale(); // process glyph runs for (const QGlyphRun &run : runs) { const auto glyphs = run.glyphIndexes(); const auto pos = run.positions(); Q_ASSERT(glyphs.size() == pos.size()); const bool doubleGlyphResolution = m_glyphCache->doubleGlyphResolution(run.rawFont()); // faithfully copied from QSGDistanceFieldGlyphNode::updateGeometry() const qreal pixelSize = run.rawFont().pixelSize(); const qreal fontScale = pixelSize / QT_DISTANCEFIELD_BASEFONTSIZE(doubleGlyphResolution); const qreal margin = QT_DISTANCEFIELD_RADIUS(doubleGlyphResolution) / QT_DISTANCEFIELD_SCALE(doubleGlyphResolution) * fontScale; for (int i = 0; i < glyphs.size(); i++) { const QDistanceFieldGlyphCache::Glyph &dfield = m_glyphCache->refGlyph(run.rawFont(), glyphs[i]); if (!dfield.texture) continue; RenderData &data = renderData[dfield.texture]; // faithfully copied from QSGDistanceFieldGlyphNode::updateGeometry() QRectF metrics = scaleRectF(dfield.glyphPathBoundingRect, fontScale); metrics.adjust(-margin, margin, margin, 3*margin); const qreal top = 0.0; const qreal left = 0.0; const qreal right = qreal(m_width); const qreal bottom = qreal(m_height); qreal x1 = left + scale * (pos[i].x() + metrics.left()); qreal y2 = bottom - scale * (pos[i].y() - metrics.top()); qreal x2 = x1 + scale * metrics.width(); qreal y1 = y2 - scale * metrics.height(); // only draw glyphs that are at least partly visible if (y2 < top || x1 > right) continue; QRectF texCoords = dfield.texCoords; // if a glyph is only partly visible within the given rectangle, // cut it in half and adjust tex coords if (y1 < top) { const auto insideRatio = (top - y2) / (y1 - y2); y1 = top; texCoords.setHeight(texCoords.height() * insideRatio); } // do the same thing horizontally if (x2 > right) { const auto insideRatio = (right - x1) / (x2 - x1); x2 = right; texCoords.setWidth(texCoords.width() * insideRatio); } for (auto v: std::vector{x1, y1, qreal(i), texCoords.left(), texCoords.bottom()}) data.vertex.push_back(float(v)); for (auto v: std::vector{x1, y2, qreal(i), texCoords.left(), texCoords.top()}) data.vertex.push_back(float(v)); for (auto v: std::vector{x2, y1, qreal(i), texCoords.right(), texCoords.bottom()}) data.vertex.push_back(float(v)); for (auto v: std::vector{x2, y2, qreal(i), texCoords.right(), texCoords.top()}) data.vertex.push_back(float(v)); for (int i: std::vector{data.vertexCount, data.vertexCount + 3, data.vertexCount + 1}) data.index.push_back(quint16(i)); for (int i: std::vector{data.vertexCount, data.vertexCount + 2, data.vertexCount + 3}) data.index.push_back(quint16(i)); data.vertexCount += 4; } } // de-ref all glyphs for previous QGlyphRuns for (int i = 0; i < m_currentGlyphRuns.size(); i++) m_glyphCache->derefGlyphs(m_currentGlyphRuns[i]); m_currentGlyphRuns = runs; // make sure we have the correct number of DistanceFieldTextRenderers // TODO: we might keep one renderer at all times, so we won't delete and // re-allocate one every time the text changes from an empty to a non-empty string // and vice-versa while (m_renderers.size() > renderData.size()) delete m_renderers.takeLast(); while (m_renderers.size() < renderData.size()) { DistanceFieldTextRenderer *renderer = new DistanceFieldTextRenderer(); renderer->setColor(m_color); renderer->setParent(q_func()); m_renderers << renderer; } Q_ASSERT(m_renderers.size() == renderData.size()); // assign vertex data for all textures to the renderers int rendererIdx = 0; for (auto it = renderData.begin(); it != renderData.end(); ++it) m_renderers[rendererIdx++]->setGlyphData(it.key(), it.value().vertex, it.value().index); } void QText2DEntityPrivate::clearCurrentGlyphRuns() { for (int i = 0; i < m_currentGlyphRuns.size(); i++) m_glyphCache->derefGlyphs(m_currentGlyphRuns[i]); m_currentGlyphRuns.clear(); } void QText2DEntityPrivate::updateGlyphs() { if (m_glyphCache == nullptr) return; QList glyphRuns; // collect all GlyphRuns generated by the QTextLayout if ((m_width > 0.0f || m_height > 0.0f) && !m_text.isEmpty()) { QTextLayout layout(m_text, m_scaledFont); QTextOption textOption = layout.textOption(); textOption.setAlignment(m_alignment); layout.setTextOption(textOption); const float lineWidth = m_width / computeActualScale(); float height = 0; layout.beginLayout(); while (true) { QTextLine line = layout.createLine(); if (!line.isValid()) break; // position current line line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); height += line.height(); // add glyph runs created by line const QList runs = line.glyphRuns(); for (const QGlyphRun &run : runs) glyphRuns << run; } layout.endLayout(); } setCurrentGlyphRuns(glyphRuns); } /*! \property Qt3DExtras::QText2DEntity::font Holds the font for the text item that is displayed in the Qt Quick scene. */ QFont QText2DEntity::font() const { Q_D(const QText2DEntity); return d->m_font; } void QText2DEntity::setFont(const QFont &font) { Q_D(QText2DEntity); if (d->m_font != font) { // ignore the point size of the font, just make it a default value. // still we want to make sure that font() returns the same value // that was passed to setFont(), so we store it nevertheless d->m_font = font; d->m_scaledFont = font; d->m_scaledFont.setPointSize(10); emit fontChanged(font); if (!d->m_text.isEmpty()) d->updateGlyphs(); } } /*! \property Qt3DExtras::QText2DEntity::color Holds the color for the text item that is displayed in the Qt Quick scene. */ QColor QText2DEntity::color() const { Q_D(const QText2DEntity); return d->m_color; } void QText2DEntity::setColor(const QColor &color) { Q_D(QText2DEntity); if (d->m_color != color) { d->m_color = color; emit colorChanged(color); for (DistanceFieldTextRenderer *renderer : std::as_const(d->m_renderers)) renderer->setColor(color); } } /*! \property Qt3DExtras::QText2DEntity::text Holds the text that is displayed in the Qt Quick scene. */ QString QText2DEntity::text() const { Q_D(const QText2DEntity); return d->m_text; } void QText2DEntity::setText(const QString &text) { Q_D(QText2DEntity); if (d->m_text != text) { d->m_text = text; emit textChanged(text); d->updateGlyphs(); } } /*! \property Qt3DExtras::QText2DEntity::width Returns the width of the text item that is displayed in the Qt Quick scene. */ float QText2DEntity::width() const { Q_D(const QText2DEntity); return d->m_width; } /*! \property Qt3DExtras::QText2DEntity::height Returns the height of the text item that is displayed in the Qt Quick scene. */ float QText2DEntity::height() const { Q_D(const QText2DEntity); return d->m_height; } void QText2DEntity::setWidth(float width) { Q_D(QText2DEntity); if (width != d->m_width) { d->m_width = width; emit widthChanged(width); d->updateGlyphs(); } } void QText2DEntity::setHeight(float height) { Q_D(QText2DEntity); if (height != d->m_height) { d->m_height = height; emit heightChanged(height); d->updateGlyphs(); } } /*! \property Qt3DExtras::QText2DEntity::alignment Returns the alignment of the text item that is displayed in the Qt Quick scene. The default alignment is top-left-aligned. */ Qt::Alignment QText2DEntity::alignment() const { Q_D(const QText2DEntity); return d->m_alignment; } void QText2DEntity::setAlignment(Qt::Alignment alignment) { Q_D(QText2DEntity); if (alignment != d->m_alignment) { d->m_alignment = alignment; d->updateGlyphs(); } } } // namespace Qt3DExtras QT_END_NAMESPACE #include "moc_qtext2dentity.cpp"