From 568a4205a66da527547e4afd15274bfcd905ae5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 20 Oct 2023 16:54:28 +0200 Subject: [PATCH] Text: new, reusable and composable guts for the Renderer. The awful original STL-heavy public API is kept the same for now, it's just the internals being now implemented using brand new APIs that are actually usable with multiple fonts, font sizes and runs with different scripts/languages/directions. There's also preparation for configurable vertical text layouting, although for now the functionality asserts that horizontal text is used. This also makes Renderer.h header available on non-GL builds, as the new APIs don't rely on a class full of GL objects anymore. The class will get eventually renamed and moved to a dedicated RendererGL.h header, but for now this partial update has to suffice. --- src/Magnum/Text/CMakeLists.txt | 8 +- src/Magnum/Text/Renderer.cpp | 406 +++++++++----- src/Magnum/Text/Renderer.h | 220 +++++++- src/Magnum/Text/Test/RendererGLTest.cpp | 50 +- src/Magnum/Text/Test/RendererTest.cpp | 707 +++++++++++++++++++++--- 5 files changed, 1152 insertions(+), 239 deletions(-) diff --git a/src/Magnum/Text/CMakeLists.txt b/src/Magnum/Text/CMakeLists.txt index c06642d5b..7071783e2 100644 --- a/src/Magnum/Text/CMakeLists.txt +++ b/src/Magnum/Text/CMakeLists.txt @@ -40,6 +40,7 @@ set(MagnumText_GracefulAssert_SRCS AbstractGlyphCache.cpp AbstractShaper.cpp Feature.cpp + Renderer.cpp Script.cpp) set(MagnumText_HEADERS @@ -50,6 +51,7 @@ set(MagnumText_HEADERS Alignment.h Direction.h Feature.h + Renderer.h Script.h Text.h @@ -62,12 +64,10 @@ if(MAGNUM_TARGET_GL) list(APPEND MagnumText_SRCS GlyphCache.cpp) list(APPEND MagnumText_GracefulAssert_SRCS - DistanceFieldGlyphCache.cpp - Renderer.cpp) + DistanceFieldGlyphCache.cpp) list(APPEND MagnumText_HEADERS DistanceFieldGlyphCache.h - GlyphCache.h - Renderer.h) + GlyphCache.h) else() # So MagnumTextObjects has at least something list(APPEND MagnumText_SRCS ${PROJECT_SOURCE_DIR}/src/dummy.cpp) diff --git a/src/Magnum/Text/Renderer.cpp b/src/Magnum/Text/Renderer.cpp index f518b2882..391fcac8a 100644 --- a/src/Magnum/Text/Renderer.cpp +++ b/src/Magnum/Text/Renderer.cpp @@ -25,46 +25,237 @@ #include "Renderer.h" +#include +#include + +#include "Magnum/Math/Functions.h" +#include "Magnum/Text/AbstractFont.h" +#include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/Direction.h" + +#ifdef MAGNUM_TARGET_GL #include #include /** @todo remove once Renderer is STL-free */ -#include #include /** @todo remove once Renderer is STL-free */ -#include #include "Magnum/Mesh.h" #include "Magnum/GL/Context.h" #include "Magnum/GL/Extensions.h" #include "Magnum/GL/Mesh.h" -#include "Magnum/Math/Functions.h" #include "Magnum/Shaders/GenericGL.h" -#include "Magnum/Text/AbstractFont.h" -#include "Magnum/Text/AbstractGlyphCache.h" #include "Magnum/Text/AbstractShaper.h" +#endif namespace Magnum { namespace Text { +Range2D renderLineGlyphPositionsInto(const AbstractFont& font, const Float size, const LayoutDirection direction, const Containers::StridedArrayView1D& glyphOffsets, const Containers::StridedArrayView1D& glyphAdvances, Vector2& cursor, const Containers::StridedArrayView1D& glyphPositions) { + CORRADE_ASSERT(glyphAdvances.size() == glyphOffsets.size() && + glyphPositions.size() == glyphOffsets.size(), + "Text::renderLineGlyphPositionsInto(): expected glyphOffsets, glyphAdvances and output views to have the same size, got" << glyphOffsets.size() << Debug::nospace << "," << glyphAdvances.size() << "and" << glyphPositions.size(), {}); + CORRADE_ASSERT(direction == LayoutDirection::HorizontalTopToBottom, + "Text::renderLineGlyphPositionsInto(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << direction, {}); + #ifdef CORRADE_NO_ASSERT + static_cast(direction); /** @todo drop once implemented */ + #endif + + CORRADE_ASSERT(font.isOpened(), + "Text::renderLineGlyphPositionsInto(): no font opened", {}); + const Float scale = size/font.size(); + + /* Combine the offsets and cursor advances and calculate the line rectangle + along the way. Initially the cursor is at origin and rectangle is empty, + with just the Y bounds from font metrics. */ + Range2D rectangle{cursor + Vector2::yAxis(font.descent()*scale), + cursor + Vector2::yAxis(font.ascent()*scale)}; + for(UnsignedInt i = 0; i != glyphOffsets.size(); ++i) { + /* The glyphOffsets and output are allowed to be aliased, so make sure + the value isn't stomped on when writing the output */ + glyphPositions[i] = cursor + glyphOffsets[i]*scale; + cursor += glyphAdvances[i]*scale; + + /* Extend the line rectangle with the cursor range */ + /** @todo this assumes left-to-right direction, update when vertical + and LTR text is possible & testable */ + rectangle.max() = Math::max(rectangle.max(), cursor); + } + + return rectangle; +} + namespace { -template void createIndices(void* output, const UnsignedInt glyphCount) { - T* const out = reinterpret_cast(output); +Range2D renderGlyphQuadsInto(const AbstractFont& font, const Float size, const AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphPositions, const Containers::StridedArrayView1D& glyphIds, const Containers::StridedArrayView1D& vertexPositions, const Containers::StridedArrayView1D& vertexTextureCoordinates, const Containers::StridedArrayView1D& vertexTextureLayers) { + CORRADE_ASSERT(glyphIds.size() == glyphPositions.size(), + "Text::renderGlyphQuadsInto(): expected glyphIds and glyphPositions views to have the same size, got" << glyphIds.size() << "and" << glyphPositions.size(), {}); + CORRADE_ASSERT(vertexPositions.size() == glyphPositions.size()*4 && + vertexTextureCoordinates.size() == glyphPositions.size()*4, + "Text::renderGlyphQuadsInto(): expected vertexPositions and vertexTextureCoordinates views to have" << glyphPositions.size()*4 << "elements, got" << vertexPositions.size() << "and" << vertexTextureCoordinates.size(), {}); + /* Should be ensured by the callers below */ + CORRADE_INTERNAL_ASSERT(!vertexTextureLayers || vertexTextureLayers.size() == vertexTextureCoordinates.size()); + + CORRADE_ASSERT(font.isOpened(), + "Text::renderGlyphQuadsInto(): no font opened", {}); + const Float scale = size/font.size(); + const Vector2 inverseCacheSize = 1.0f/Vector2{cache.size().xy()}; + + const Containers::Optional fontId = cache.findFont(&font); + CORRADE_ASSERT(fontId, + "Text::renderGlyphQuadsInto(): font not found among" << cache.fontCount() << "fonts in passed glyph cache", {}); + + /* Get all glyphs from the glyph cache, create quads for each and calculate + the glyph bound rectangle along the way. */ + Range2D rectangle; + for(std::size_t i = 0; i != glyphIds.size(); ++i) { + /* Offset of the glyph rectangle relative to the cursor, layer, + texture coordinates. We checked that the glyph cache is 2D above + so the layer can be ignored. */ + const Containers::Triple cacheGlyph = cache.glyph(*fontId, glyphIds[i]); + + /* 2---3 + | | + | | + | | + 0---1 */ + const Range2D quad = Range2D::fromSize( + glyphPositions[i] + Vector2{cacheGlyph.first()}*scale, + Vector2{cacheGlyph.third().size()}*scale); + const Range2D texture = Range2D{cacheGlyph.third()} + .scaled(inverseCacheSize); + const std::size_t i4 = i*4; + for(UnsignedByte j = 0; j != 4; ++j) { + /* ✨ */ + vertexPositions[i4 + j] = Math::lerp(quad.min(), quad.max(), BitVector2{j}); + vertexTextureCoordinates[i4 + j] = Math::lerp(texture.min(), texture.max(), BitVector2{j}); + } + + /* Fill also a texture layer if desirable. For 2D output the caller + already checked that the cache is 2D. */ + if(vertexTextureLayers) for(std::size_t j = 0; j != 4; ++j) + vertexTextureLayers[i4 + j] = cacheGlyph.second(); + + /* Extend the rectangle with current glyph bounds */ + rectangle = Math::join(rectangle, quad); + } + + return rectangle; +} + +} + +Range2D renderGlyphQuadsInto(const AbstractFont& font, const Float size, const AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphPositions, const Containers::StridedArrayView1D& glyphIds, const Containers::StridedArrayView1D& vertexPositions, const Containers::StridedArrayView1D& vertexTextureCoordinates) { + return renderGlyphQuadsInto(font, size, cache, glyphPositions, glyphIds, vertexPositions, vertexTextureCoordinates.slice(&Vector3::xy), vertexTextureCoordinates.slice(&Vector3::z)); +} + +Range2D renderGlyphQuadsInto(const AbstractFont& font, const Float size, const AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphPositions, const Containers::StridedArrayView1D& glyphIds, const Containers::StridedArrayView1D& vertexPositions, const Containers::StridedArrayView1D& vertexTextureCoordinates) { + CORRADE_ASSERT(cache.size().z() == 1, + "Text::renderGlyphQuadsInto(): can't use this overload with an array glyph cache", {}); + return renderGlyphQuadsInto(font, size, cache, glyphPositions, glyphIds, vertexPositions, vertexTextureCoordinates, nullptr); +} + +Range2D alignRenderedLine(const Range2D& lineRectangle, const LayoutDirection direction, const Alignment alignment, const Containers::StridedArrayView1D& positions) { + CORRADE_ASSERT(direction == LayoutDirection::HorizontalTopToBottom, + "Text::alignRenderedLine(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << direction, {}); + #ifdef CORRADE_NO_ASSERT + static_cast(direction); /** @todo drop once implemented */ + #endif + + /** @todo this again assumes horizontal direction, needs to be updated once + vertical (and possibly mixed horizontal/vertical) text is possible */ + + Float alignmentOffsetX = 0.0f; + if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentLeft) + alignmentOffsetX = -lineRectangle.left(); + else if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentCenter) { + alignmentOffsetX = -lineRectangle.centerX(); + /* Integer alignment */ + if(UnsignedByte(alignment) & Implementation::AlignmentIntegral) + alignmentOffsetX = Math::round(alignmentOffsetX); + } + else if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentRight) + alignmentOffsetX = -lineRectangle.right(); + + /* Shift all positions */ + for(Vector2& i: positions) + i.x() += alignmentOffsetX; + + return lineRectangle.translated(Vector2::xAxis(alignmentOffsetX)); +} + +Range2D alignRenderedBlock(const Range2D& blockRectangle, const LayoutDirection direction, const Alignment alignment, const Containers::StridedArrayView1D& positions) { + CORRADE_ASSERT(direction == LayoutDirection::HorizontalTopToBottom, + "Text::alignRenderedBlock(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << direction, {}); + #ifdef CORRADE_NO_ASSERT + static_cast(direction); /** @todo drop once implemented */ + #endif + + /** @todo this assumes vertical layout advance, needs to be updated once + other directions are possible */ + + Float alignmentOffsetY = 0.0f; + if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentBottom) + alignmentOffsetY = -blockRectangle.bottom(); + else if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentMiddle) { + alignmentOffsetY = -blockRectangle.centerY(); + /* Integer alignment */ + if(UnsignedByte(alignment) & Implementation::AlignmentIntegral) + alignmentOffsetY = Math::round(alignmentOffsetY); + } + else if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentTop) + alignmentOffsetY = -blockRectangle.top(); + + /* Shift all positions */ + for(Vector2& i: positions) + i.y() += alignmentOffsetY; + + return blockRectangle.translated(Vector2::yAxis(alignmentOffsetY)); +} + +namespace { + +template void renderGlyphQuadIndicesIntoInternal(const UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices) { + CORRADE_ASSERT(indices.size() % 6 == 0, + "Text::renderGlyphQuadIndicesInto(): expected the indices view size to be divisible by 6, got" << indices.size(), ); + const UnsignedInt glyphCount = indices.size()/6; + #ifndef CORRADE_NO_ASSERT + const UnsignedLong maxValue = UnsignedLong(glyphOffset)*4 + UnsignedLong(glyphCount)*4; + #endif + CORRADE_ASSERT(maxValue <= (1ull << 8*sizeof(T)), + "Text::renderGlyphQuadIndicesInto(): max index value of" << maxValue - 1 << "cannot fit into a" << 8*sizeof(T) << Debug::nospace << "-bit type", ); + for(UnsignedInt i = 0; i != glyphCount; ++i) { - /* 0---2 0---2 5 - | | | / /| - | | | / / | - | | |/ / | - 1---3 1 3---4 */ - - const T vertex = T(i)*4; - const UnsignedInt pos = T(i)*6; - out[pos] = vertex; - out[pos+1] = vertex+1; - out[pos+2] = vertex+2; - out[pos+3] = vertex+1; - out[pos+4] = vertex+3; - out[pos+5] = vertex+2; + /* 2---3 2 3---5 + | | |\ \ | + | | | \ \ | + | | | \ \| + 0---1 0---1 4 */ + const UnsignedInt i4 = (glyphOffset + i)*4; + const UnsignedInt i6 = i*6; + indices[i6 + 0] = i4 + 0; + indices[i6 + 1] = i4 + 1; + indices[i6 + 2] = i4 + 2; + indices[i6 + 3] = i4 + 2; + indices[i6 + 4] = i4 + 1; + indices[i6 + 5] = i4 + 3; } } +} + +void renderGlyphQuadIndicesInto(UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices) { + renderGlyphQuadIndicesIntoInternal(glyphOffset, indices); +} + +void renderGlyphQuadIndicesInto(UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices) { + renderGlyphQuadIndicesIntoInternal(glyphOffset, indices); +} + +void renderGlyphQuadIndicesInto(UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices) { + renderGlyphQuadIndicesIntoInternal(glyphOffset, indices); +} + +#ifdef MAGNUM_TARGET_GL +namespace { + struct Vertex { Vector2 position, textureCoordinates; }; @@ -77,10 +268,9 @@ std::tuple, Range2D> renderVerticesInternal(AbstractFont& fo CORRADE_ASSERT(cache.size().z() == 1, "Text::Renderer: array glyph caches are not supported", {}); - /* Find this font in the cache. This is an assert again, as not having a - font in the cache is a user error. */ - Containers::Optional fontId = cache.findFont(&font); - CORRADE_ASSERT(fontId, + /* Find this font in the cache and assert in the high-level API already to + avoid confusion */ + CORRADE_ASSERT(cache.findFont(&font), "Text::Renderer: font not found among" << cache.fontCount() << "fonts in passed glyph cache", {}); /* Output data, reserve memory as when the text would be ASCII-only. In @@ -95,7 +285,6 @@ std::tuple, Range2D> renderVerticesInternal(AbstractFont& fo const Vector2 lineAdvance = Vector2::yAxis(font.lineHeight()*scale); Range2D rectangle; Vector2 linePosition; - std::size_t lastLineLastVertex = 0; /* Temp buffer so we don't allocate for each new line */ /** @@ -137,118 +326,58 @@ std::tuple, Range2D> renderVerticesInternal(AbstractFont& fo issue arises. */ CORRADE_INTERNAL_ASSERT(vertices.size() + shaper->glyphCount()*4 <= vertices.capacity()); - /* Bounds of rendered line. If `Alignment::*GlyphBounds` is used, it's - filled with actual bounds of each glyph, otherwise with - ascent/descent and actual cursor range. */ - Range2D lineRectangle; - /** @todo this assumes horizontal direction, update when vertical text - is possible & testable */ - if(!(UnsignedByte(alignment) & Implementation::AlignmentGlyphBounds)) - lineRectangle = {linePosition + Vector2::yAxis(font.descent()*scale), - linePosition + Vector2::yAxis(font.ascent()*scale)}; - - /* Create quads for all glyphs */ - Vector2 cursorPosition(linePosition); - for(UnsignedInt i = 0; i != lineGlyphs.size(); ++i) { - /* Offset of the glyph rectangle relative to the cursor, layer, - texture coordinates. We checked that the glyph cache is 2D above - so the layer can be ignored. */ - const Containers::Triple cacheGlyph = cache.glyph(*fontId, glyphs[i].id); - CORRADE_INTERNAL_ASSERT(cacheGlyph.second() == 0); - - /* Quad rectangle, created from cache and shaper offset and the - texture rectangle, scaled to requested text size and translated - to current cursor */ - const Range2D quadPosition = Range2D::fromSize( - Vector2{cacheGlyph.first()} + glyphs[i].offset, - Vector2{cacheGlyph.third().size()}) - .scaled(Vector2{scale}) - .translated(cursorPosition); - - /* Normalized texture coordinates */ - const Range2D quadTextureCoordinates = Range2D{cacheGlyph.third()} - .scaled(1.0f/Vector2{cache.size().xy()}); - - /* 0---2 - | | - | | - | | - 1---3 */ - vertices.insert(vertices.end(), { - {quadPosition.topLeft(), quadTextureCoordinates.topLeft()}, - {quadPosition.bottomLeft(), quadTextureCoordinates.bottomLeft()}, - {quadPosition.topRight(), quadTextureCoordinates.topRight()}, - {quadPosition.bottomRight(), quadTextureCoordinates.bottomRight()} - }); - - /* Advance cursor position to next character, again scaled */ - cursorPosition += glyphs[i].advance*scale; - - /* Extend the line rectangle with current glyph bounds if - `Alignment::*GlyphBounds` is used, otherwise just expand with - the cursor range. */ - if(UnsignedByte(alignment) & Implementation::AlignmentGlyphBounds) { - /* If the original is zero size, it gets replaced */ - lineRectangle = Math::join(lineRectangle, quadPosition); - } else { - /** @todo this assumes left-to-right direction, update when - when vertical text is possible & testable */ - lineRectangle.max() = Math::max(lineRectangle.max(), cursorPosition); - } - } - - /** @todo What about top-down text? */ - - /* Horizontally align the rendered line. As we have the `lineRectangle` - already appropriate based on presence of `Alignment::*GlyphBounds`, - we don't need to special-case it here in any way. */ - Float alignmentOffsetX = 0.0f; - if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentLeft) - alignmentOffsetX = -lineRectangle.left(); - else if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentCenter) { - alignmentOffsetX = -lineRectangle.centerX(); - /* Integer alignment */ - if(UnsignedByte(alignment) & Implementation::AlignmentIntegral) - alignmentOffsetX = Math::round(alignmentOffsetX); - } - else if((UnsignedByte(alignment) & Implementation::AlignmentHorizontal) == Implementation::AlignmentRight) - alignmentOffsetX = -lineRectangle.right(); - - /* Align positions and bounds on current line */ - lineRectangle = lineRectangle.translated(Vector2::xAxis(alignmentOffsetX)); - for(auto it = vertices.begin()+lastLineLastVertex; it != vertices.end(); ++it) - it->position.x() += alignmentOffsetX; - - /* Extend the rectangle with final line bounds. This is again the same - code path for both with and without `Alignment::*GlyphBounds`. */ - rectangle = Math::join(rectangle, lineRectangle); + Vector2 cursor = linePosition; + + /* Render line glyph positions into the first vertex of each quad in + the output */ + vertices.resize(vertices.size() + shaper->glyphCount()*4); + const Containers::StridedArrayView1D lineVertices = Containers::stridedArrayView(vertices).exceptPrefix(vertices.size() - shaper->glyphCount()*4); + const Range2D lineRectangle = renderLineGlyphPositionsInto( + font, + size, + /** @todo direction hardcoded here */ + LayoutDirection::HorizontalTopToBottom, + lineGlyphs.slice(&Glyph::offset), + lineGlyphs.slice(&Glyph::advance), + cursor, + lineVertices.slice(&Vertex::position).every(4)); + + /* Create quads from the positions */ + const Range2D lineQuadRectangle = renderGlyphQuadsInto( + font, + size, + cache, + lineVertices.slice(&Vertex::position).every(4), + lineGlyphs.slice(&Glyph::id), + lineVertices.slice(&Vertex::position), + lineVertices.slice(&Vertex::textureCoordinates)); + + /* Horizontally align the line, using either of the rectangles based on + which alignment is desired */ + const Range2D alignedLineRectangle = alignRenderedLine( + UnsignedByte(alignment) & Implementation::AlignmentGlyphBounds ? + lineQuadRectangle : lineRectangle, + /** @todo direction hardcoded here */ + LayoutDirection::HorizontalTopToBottom, + alignment, + lineVertices.slice(&Vertex::position)); + + /* Extend the rectangle with final line bounds */ + rectangle = Math::join(rectangle, alignedLineRectangle); /* Move to next line */ } while(prevPos = pos+1, linePosition -= lineAdvance, - lastLineLastVertex = vertices.size(), pos != std::string::npos); - /* Vertically align the rendered text. Again, as we had the input rects - already appropriate based on presence of `Alignment::*GlyphBounds`, we - don't need to special-case it here in any way either. */ - Float alignmentOffsetY = 0.0f; - if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentBottom) - alignmentOffsetY = -rectangle.bottom(); - else if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentMiddle) { - alignmentOffsetY = -rectangle.centerY(); - /* Integer alignment */ - if(UnsignedByte(alignment) & Implementation::AlignmentIntegral) - alignmentOffsetY = Math::round(alignmentOffsetY); - } - else if((UnsignedByte(alignment) & Implementation::AlignmentVertical) == Implementation::AlignmentTop) - alignmentOffsetY = -rectangle.top(); + const Range2D alignedRectangle = alignRenderedBlock( + rectangle, + /** @todo direction hardcoded here */ + LayoutDirection::HorizontalTopToBottom, + alignment, + Containers::stridedArrayView(vertices).slice(&Vertex::position)); - /* Align positions and bounds */ - rectangle = rectangle.translated(Vector2::yAxis(alignmentOffsetY)); - for(auto& v: vertices) v.position.y() += alignmentOffsetY; - - return std::make_tuple(Utility::move(vertices), rectangle); + return std::make_tuple(Utility::move(vertices), alignedRectangle); } std::pair, MeshIndexType> renderIndicesInternal(const UnsignedInt glyphCount) { @@ -259,16 +388,16 @@ std::pair, MeshIndexType> renderIndicesInternal(const Un MeshIndexType indexType; if(vertexCount <= 256) { indexType = MeshIndexType::UnsignedByte; - indices = Containers::Array(indexCount*sizeof(UnsignedByte)); - createIndices(indices, glyphCount); + indices = Containers::Array{NoInit, indexCount*sizeof(UnsignedByte)}; + renderGlyphQuadIndicesInto(0, Containers::arrayCast(indices)); } else if(vertexCount <= 65536) { indexType = MeshIndexType::UnsignedShort; - indices = Containers::Array(indexCount*sizeof(UnsignedShort)); - createIndices(indices, glyphCount); + indices = Containers::Array{NoInit, indexCount*sizeof(UnsignedShort)}; + renderGlyphQuadIndicesInto(0, Containers::arrayCast(indices)); } else { indexType = MeshIndexType::UnsignedInt; - indices = Containers::Array(indexCount*sizeof(UnsignedInt)); - createIndices(indices, glyphCount); + indices = Containers::Array{NoInit, indexCount*sizeof(UnsignedInt)}; + renderGlyphQuadIndicesInto(0, Containers::arrayCast(indices)); } return {Utility::move(indices), indexType}; @@ -320,7 +449,7 @@ std::tuple, std::vector, std::vector, /* Render indices */ const UnsignedInt glyphCount = vertices.size()/4; std::vector indices(glyphCount*6); - createIndices(indices.data(), glyphCount); + renderGlyphQuadIndicesInto(0, indices); return std::make_tuple(Utility::move(positions), Utility::move(textureCoordinates), Utility::move(indices), rectangle); } @@ -461,5 +590,6 @@ void AbstractRenderer::render(const std::string& text) { template class MAGNUM_TEXT_EXPORT Renderer<2>; template class MAGNUM_TEXT_EXPORT Renderer<3>; #endif +#endif }} diff --git a/src/Magnum/Text/Renderer.h b/src/Magnum/Text/Renderer.h index 4827a5134..639a2ef26 100644 --- a/src/Magnum/Text/Renderer.h +++ b/src/Magnum/Text/Renderer.h @@ -26,10 +26,11 @@ */ /** @file Text/Renderer.h - * @brief Class @ref Magnum::Text::AbstractRenderer, @ref Magnum::Text::Renderer, typedef @ref Magnum::Text::Renderer2D, @ref Magnum::Text::Renderer3D + * @brief Class @ref Magnum::Text::AbstractRenderer, @ref Magnum::Text::Renderer, typedef @ref Magnum::Text::Renderer2D, @ref Magnum::Text::Renderer3D, function @ref Magnum::Text::renderLineGlyphPositionsInto(), @ref Magnum::Text::renderGlyphQuadsInto() @ref Magnum::Text::alignRenderedLine(), @ref Magnum::Text::alignRenderedBlock() */ -#include "Magnum/configure.h" +#include "Magnum/Text/Text.h" +#include "Magnum/Text/visibility.h" #ifdef MAGNUM_TARGET_GL #include @@ -40,16 +41,223 @@ #include "Magnum/Math/Range.h" #include "Magnum/GL/Buffer.h" #include "Magnum/GL/Mesh.h" -#include "Magnum/Text/Text.h" #include "Magnum/Text/Alignment.h" -#include "Magnum/Text/visibility.h" #ifdef CORRADE_TARGET_EMSCRIPTEN #include #endif +#endif namespace Magnum { namespace Text { +/** +@brief Render glyph positions for a (part of a) single line +@param[in] font Font to query metrics from +@param[in] size Size to render the glyphs at +@param[in] direction Layout direction. Currently expected to always be + @ref LayoutDirection::HorizontalTopToBottom. +@param[in] glyphOffsets Glyph offsets coming from @ref AbstractShaper + instance(s) associated with @p font +@param[in] glyphAdvances Glyph advances coming from @ref AbstractShaper + instance(s) associated with @p font +@param[in,out] cursor Initial cursor position. Is updated to a final + cursor position after all glyphs are rendered. +@param[out] glyphPositions Where to put output absolute glyph positions +@return Rectangle spanning the rendered cursor range in one direction and font + descent to ascent in the other +@m_since_latest + +The output of this function are just glyph positions alone, which is useful for +example when the actual glyph quad expansion is done by a shader or when the +glyphs get subsequently rasterized some other way than applying a glyph texture +to a sequence of quads. Use @ref renderGlyphQuadsInto() on the resulting +@p glyphPositions array to form actual glyph quads together with texture +coordinates. + +The @p glyphOffsets, @p glyphAdvances and @p glyphPositions views are all +expected to have the same size. It's possible to use the same view for +@p glyphOffsets and @p glyphPositions, which will turn the input relative glyph +offsets into absolute positions. + +Calls to this function don't strictly need to match calls to +@ref AbstractShaper::shape(). For example if multiple text runs on a single +line differ just by script, language or direction but not by a font or +rendering size, they can be shaped into consecutive portions of a larger +@p glyphOffsets and @p glyphAdvances array and this function can be then called +just once for all runs together. If the font or rendering size changes between +text runs however, you have to call this function for each such run separately +and each time use the updated @p cursor value as an input for the next +@ref renderLineGlyphPositionsInto() call. + +@m_class{m-note m-warning} + +@par + This function only works on a single line of text. When rendering a + multi-line text, you have to split it by lines and then shape, render and + align each individually, and adjust @p cursor for each new line as + appropriate. + +Once the whole line is rendered, @ref Math::join() the rectangles returned from +all calls to this function and pass them together with positions for the whole +line to @ref alignRenderedLine(). Finally, to align a multi-line block, join +rectangles returned from all @ref alignRenderedLine() calls and pass them +together with positions for the whole text to @ref alignRenderedBlock(). +*/ +MAGNUM_TEXT_EXPORT Range2D renderLineGlyphPositionsInto(const AbstractFont& font, Float size, LayoutDirection direction, const Containers::StridedArrayView1D& glyphOffsets, const Containers::StridedArrayView1D& glyphAdvances, Vector2& cursor, const Containers::StridedArrayView1D& glyphPositions); + +/** +@brief Render glyph quads for a (part of a) single line +@param[in] font Font to query metrics from +@param[in] size Size to render the glyphs at +@param[in] cache Glyph cache to query for glyph rectangles +@param[in] glyphPositions Glyph positions coming from an earlier call to + @ref renderLineGlyphPositionsInto() +@param[in] glyphIds Matching glyph IDs coming from + @ref AbstractShaper instance(s) associated with @p font +@param[out] vertexPositions Where to put output vertex positions +@param[out] vertexTextureCoordinates Where to put output texture coordinates +@return Rectangle spanning the rendered glyph quads +@m_since_latest + +Produces a sequence of quad corner positions and texture coordinates in order +as shown below. The @p glyphPositions and @p glyphIds views are expected to +have the same size, the @p vertexPositions and @p vertexTextureCoordinates +views are then expected to be four times larger than @p glyphPositions and +@p glyphIds, in order to ultimately contain four corner vertices for each +glyph. To optimize memory use, it's possible to alias @p glyphPositions and +@p glyphIds with @cpp vertexPositions.every(4) @ce and +@cpp vertexTextureCoordinates.every(4) @ce --- the rendering is performed in a +way that first reads the position and ID for each glyph and only then fills in +the vertex data. + +@verbatim +2---3 +| | +| | +| | +0---1 +@endverbatim + +If the text doesn't need to be aligned based on the actual glyph bounds (i.e., +the desired @ref Alignment isn't `*GlyphBounds`), it's possible to call this +function even on a multi-line text run provided that @ref alignRenderedLine() +was called on the @p glyphPositions before to align lines relatively to each +other. Otherwise this function should be called on each line individually and +then the @p vertexPositions passed further to @ref alignRenderedLine(). + +Expects that @p font is contained in @p cache. Glyph IDs not found in the cache +are replaced with the cache-global invalid glyph. If the @p cache is only 2D, +you can use the @ref renderGlyphQuadsInto(const AbstractFont&, Float, const AbstractGlyphCache&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) +overload to get just 2D texture coordinates out. Use +@ref renderGlyphQuadIndicesInto() to populate the corresponding index array. +*/ +MAGNUM_TEXT_EXPORT Range2D renderGlyphQuadsInto(const AbstractFont& font, Float size, const AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphPositions, const Containers::StridedArrayView1D& glyphIds, const Containers::StridedArrayView1D& vertexPositions, const Containers::StridedArrayView1D& vertexTextureCoordinates); + +/** +@brief Render glyph quads for a (part of a) single line and a 2D glyph cache +@m_since_latest + +Compared to @ref renderGlyphQuadsInto(const AbstractFont&, Float, const AbstractGlyphCache&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) +outputs just 2D texture coordinates. Expects that @ref AbstractGlyphCache::size() +depth is @cpp 1 @ce. +*/ +MAGNUM_TEXT_EXPORT Range2D renderGlyphQuadsInto(const AbstractFont& font, Float size, const AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphPositions, const Containers::StridedArrayView1D& glyphIds, const Containers::StridedArrayView1D& vertexPositions, const Containers::StridedArrayView1D& vertexTextureCoordinates); + +/** +@brief Align a rendered line +@param[in] lineRectangle Rectangle spanning the whole line +@param[in] direction Layout direction. Currently expected to always be + @ref LayoutDirection::HorizontalTopToBottom. +@param[in] alignment Desired alignment. Only the part in direction of + the line is used. +@param[in,out] positions Positions of glyphs or glyph quad vertices on the + whole line to be aligned +@return The @p lineRectangle, translated in the direction of the line based on + the alignment. +@m_since_latest + +If @p alignment isn't `*GlyphBounds`, this function should get glyph +@p positions for the whole line coming from @ref renderLineGlyphPositionsInto() +and @p lineRectangle being all rectangles returned by that function combined +together with @ref Math::join(). + +If @p alignment is `*GlyphBounds`, this function should get vertex @p positions +for a whole line coming from @ref renderGlyphQuadsInto() and @p lineRectangle +being all rectangles returned by that function combined together with +@ref Math::join(). + +The @p positions are translated in one axis based on the @p inputRectangle and +the part of @p alignment matching line direction in @p direction. Values of the +@p positions themselves aren't considered when calculating the alignment. To +align a multi-line block, join rectangles returned from all calls to this +function and pass them together with positions for the whole block to +@ref alignRenderedBlock(). +*/ +MAGNUM_TEXT_EXPORT Range2D alignRenderedLine(const Range2D& lineRectangle, LayoutDirection direction, Alignment alignment, const Containers::StridedArrayView1D& positions); + +/** +@brief Align a rendered block +@param[in] blockRectangle Rectangle spanning all lines in the block +@param[in] direction Layout direction. Currently expected to always be + @ref LayoutDirection::HorizontalTopToBottom. +@param[in] alignment Desired alignment. Only the part in direction of + the line is used. +@param[in,out] positions Positions of glyphs or glyph quad vertices on the + whole line to be aligned +@return The @p blockRectangle, translated in the direction of the layout + advance based on the alignment. +@m_since_latest + +This function should get glyph or vertex @p positions for all lines as aligned +by calls to @ref alignRenderedLine(), and @p blockRectangle being all line +rectangles returned by that function combined together with @ref Math::join(). + +The @p positions are translated in one axis based on the @p inputRectangle and +the part of @p alignment matching layout advance in @p direction. Values of the +@p positions themselves aren't considered when calculating the translation. +*/ +MAGNUM_TEXT_EXPORT Range2D alignRenderedBlock(const Range2D& blockRectangle, LayoutDirection direction, Alignment alignment, const Containers::StridedArrayView1D& positions); + +/** +@brief Render 32-bit glyph quad indices +@param[in] glyphOffset Offset of the first glyph to generate indices for +@param[out] indices Where to put the generated indices +@m_since_latest + +Produces a sequence of quad indices in order as shown below, with the index +values being shifted by @cpp glyphOffset*4 @ce. Expects that the @p indices +view size is divisible by @cpp 6 @ce and the value range fits into the output +type. + +@verbatim +2---3 2 3---5 +| | |\ \ | +| | | \ \ | +| | | \ \| +0---1 0---1 4 +@endverbatim +*/ +MAGNUM_TEXT_EXPORT void renderGlyphQuadIndicesInto(UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices); + +/** +@brief Render 16-bit glyph quad indices +@m_since_latest + +See @ref renderGlyphQuadIndicesInto(UnsignedInt, const Containers::StridedArrayView1D&) +for more information. +*/ +MAGNUM_TEXT_EXPORT void renderGlyphQuadIndicesInto(UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices); + +/** +@brief Render 8-bit glyph quad indices +@m_since_latest + +See @ref renderGlyphQuadIndicesInto(UnsignedInt, const Containers::StridedArrayView1D&) +for more information. +*/ +MAGNUM_TEXT_EXPORT void renderGlyphQuadIndicesInto(UnsignedInt glyphOffset, const Containers::StridedArrayView1D& indices); + +#ifdef MAGNUM_TARGET_GL /** @brief Base for text renderers @@ -340,10 +548,8 @@ typedef Renderer<2> Renderer2D; for more information. */ typedef Renderer<3> Renderer3D; +#endif }} -#else -#error this header is available only in the OpenGL build -#endif #endif diff --git a/src/Magnum/Text/Test/RendererGLTest.cpp b/src/Magnum/Text/Test/RendererGLTest.cpp index ab2f35645..f9342cae8 100644 --- a/src/Magnum/Text/Test/RendererGLTest.cpp +++ b/src/Magnum/Text/Test/RendererGLTest.cpp @@ -146,28 +146,28 @@ void RendererGLTest::renderMesh() { /* Vertex buffer contents */ Containers::Array vertices = vertexBuffer.data(); CORRADE_COMPARE_AS(Containers::arrayCast(vertices), Containers::arrayView({ - Vector2{ 2.5f, 10.5f} + offset, {0.0f, 0.5f}, Vector2{ 2.5f, 5.5f} + offset, {0.0f, 0.0f}, - Vector2{12.5f, 10.5f} + offset, {1.0f, 0.5f}, Vector2{12.5f, 5.5f} + offset, {1.0f, 0.0f}, + Vector2{ 2.5f, 10.5f} + offset, {0.0f, 0.5f}, + Vector2{12.5f, 10.5f} + offset, {1.0f, 0.5f}, - Vector2{ 5.5f, 8.75f} + offset, {0.0f, 1.0f}, Vector2{ 5.5f, 3.75f} + offset, {0.0f, 0.5f}, - Vector2{10.5f, 8.75f} + offset, {0.5f, 1.0f}, Vector2{10.5f, 3.75f} + offset, {0.5f, 0.5f}, + Vector2{ 5.5f, 8.75f} + offset, {0.0f, 1.0f}, + Vector2{10.5f, 8.75f} + offset, {0.5f, 1.0f}, - Vector2{ 4.0f, 9.0f} + offset, {0.5f, 1.0f}, Vector2{ 4.0f, 4.0f} + offset, {0.5f, 0.5f}, - Vector2{ 9.0f, 9.0f} + offset, {1.0f, 1.0f}, Vector2{ 9.0f, 4.0f} + offset, {1.0f, 0.5f}, + Vector2{ 4.0f, 9.0f} + offset, {0.5f, 1.0f}, + Vector2{ 9.0f, 9.0f} + offset, {1.0f, 1.0f}, }), TestSuite::Compare::Container); Containers::Array indices = indexBuffer.data(); CORRADE_COMPARE_AS(Containers::arrayCast(indices), Containers::arrayView({ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10 + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, }), TestSuite::Compare::Container); #endif } @@ -196,9 +196,9 @@ void RendererGLTest::renderMeshIndexType() { CORRADE_COMPARE(indicesByte.size(), 64*6); CORRADE_COMPARE_AS(Containers::arrayCast(indicesByte).prefix(18), Containers::arrayView({ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10 + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, }), TestSuite::Compare::Container); /* 16-bit indices (260 vertices) */ @@ -210,9 +210,9 @@ void RendererGLTest::renderMeshIndexType() { CORRADE_COMPARE(indicesShort.size(), 65*6*2); CORRADE_COMPARE_AS(Containers::arrayCast(indicesShort).prefix(18), Containers::arrayView({ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10 + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, }), TestSuite::Compare::Container); #else CORRADE_SKIP("Can't verify buffer contents on OpenGL ES."); @@ -250,10 +250,10 @@ void RendererGLTest::mutableText() { Containers::Array indices = renderer.indexBuffer().data(); CORRADE_COMPARE_AS(Containers::arrayCast(indices).prefix(24), Containers::arrayView({ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10, - 12, 13, 14, 13, 15, 14 + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, }), TestSuite::Compare::Container); #endif @@ -272,20 +272,20 @@ void RendererGLTest::mutableText() { #ifndef MAGNUM_TARGET_GLES Containers::Array vertices = renderer.vertexBuffer().data(); CORRADE_COMPARE_AS(Containers::arrayCast(vertices).prefix(2*4*3), Containers::arrayView({ - Vector2{ 2.5f, 10.5f} + offset, {0.0f, 0.5f}, Vector2{ 2.5f, 5.5f} + offset, {0.0f, 0.0f}, - Vector2{12.5f, 10.5f} + offset, {1.0f, 0.5f}, Vector2{12.5f, 5.5f} + offset, {1.0f, 0.0f}, + Vector2{ 2.5f, 10.5f} + offset, {0.0f, 0.5f}, + Vector2{12.5f, 10.5f} + offset, {1.0f, 0.5f}, - Vector2{ 5.5f, 8.75f} + offset, {0.0f, 1.0f}, Vector2{ 5.5f, 3.75f} + offset, {0.0f, 0.5f}, - Vector2{10.5f, 8.75f} + offset, {0.5f, 1.0f}, Vector2{10.5f, 3.75f} + offset, {0.5f, 0.5f}, + Vector2{ 5.5f, 8.75f} + offset, {0.0f, 1.0f}, + Vector2{10.5f, 8.75f} + offset, {0.5f, 1.0f}, - Vector2{ 4.0f, 9.0f} + offset, {0.5f, 1.0f}, Vector2{ 4.0f, 4.0f} + offset, {0.5f, 0.5f}, - Vector2{ 9.0f, 9.0f} + offset, {1.0f, 1.0f}, Vector2{ 9.0f, 4.0f} + offset, {1.0f, 0.5f}, + Vector2{ 4.0f, 9.0f} + offset, {0.5f, 1.0f}, + Vector2{ 9.0f, 9.0f} + offset, {1.0f, 1.0f}, }), TestSuite::Compare::Container); #endif } diff --git a/src/Magnum/Text/Test/RendererTest.cpp b/src/Magnum/Text/Test/RendererTest.cpp index 5aeea60f3..7f992e06c 100644 --- a/src/Magnum/Text/Test/RendererTest.cpp +++ b/src/Magnum/Text/Test/RendererTest.cpp @@ -27,14 +27,18 @@ #include #include #include +#include /** @todo drop once Debug is stream-free */ #include #include +#include +#include #include /** @todo drop once Debug is stream-free */ #include "Magnum/PixelFormat.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" #include "Magnum/Text/AbstractShaper.h" +#include "Magnum/Text/Direction.h" #include "Magnum/Text/Renderer.h" namespace Magnum { namespace Text { namespace Test { namespace { @@ -42,6 +46,30 @@ namespace Magnum { namespace Text { namespace Test { namespace { struct RendererTest: TestSuite::Tester { explicit RendererTest(); + void lineGlyphPositions(); + void lineGlyphPositionsAliasedViews(); + void lineGlyphPositionsInvalidViewSizes(); + void lineGlyphPositionsInvalidDirection(); + void lineGlyphPositionsNoFontOpened(); + + void lineGlyphQuads(); + void lineGlyphQuadsAliasedViews(); + void lineGlyphQuadsInvalidViewSizes(); + void lineGlyphQuadsNoFontOpened(); + void lineGlyphQuadsFontNotFoundInCache(); + + void lineGlyphQuads2D(); + void lineGlyphQuads2DArrayGlyphCache(); + + void alignLine(); + void alignLineInvalidDirection(); + + void alignBlock(); + void alignBlockInvalidDirection(); + + template void glyphQuadIndices(); + void glyphQuadIndicesTypeTooSmall(); + void renderData(); void multiline(); @@ -52,6 +80,37 @@ struct RendererTest: TestSuite::Tester { #endif }; +const struct { + const char* name; + Alignment alignment; + Float offset; +} AlignLineData[]{ + /* The vertical alignment and GlyphBounds has no effect here */ + /* Left is the default (0) value, thus should result in no shift */ + {"left", Alignment::BottomLeft, -10.0f}, + {"right", Alignment::LineRightGlyphBounds, -13.5f}, + /* Integral should be handled only for Center */ + {"right, integral", Alignment::MiddleRightGlyphBoundsIntegral, -13.5f}, + {"center", Alignment::TopCenter, -11.75f}, + {"center, integral", Alignment::TopCenterIntegral, -12.0f}, +}; + +const struct { + const char* name; + Alignment alignment; + Float offset; +} AlignBlockData[]{ + /* The horizontal alignment and GlyphBounds has no effect here */ + /* Line is the default (0) value, thus should result in no shift */ + {"line", Alignment::LineCenterGlyphBounds, 0.0f}, + {"bottom", Alignment::BottomRight, -9.5f}, + {"top", Alignment::TopLeftGlyphBounds, -19.5f}, + /* Integral should be handled only for Middle */ + {"top, integral", Alignment::TopCenterGlyphBoundsIntegral, -19.5f}, + {"middle", Alignment::MiddleLeft, -14.5f}, + {"middle, integral", Alignment::MiddleLeftIntegral, -15.0f} +}; + const struct { TestSuite::TestCaseDescriptionSourceLocation name; Alignment alignment; @@ -189,6 +248,36 @@ const struct { }; RendererTest::RendererTest() { + addTests({&RendererTest::lineGlyphPositions, + &RendererTest::lineGlyphPositionsAliasedViews, + &RendererTest::lineGlyphPositionsInvalidViewSizes, + &RendererTest::lineGlyphPositionsInvalidDirection, + &RendererTest::lineGlyphPositionsNoFontOpened, + + &RendererTest::lineGlyphQuads, + &RendererTest::lineGlyphQuadsAliasedViews, + &RendererTest::lineGlyphQuadsInvalidViewSizes, + &RendererTest::lineGlyphQuadsNoFontOpened, + &RendererTest::lineGlyphQuadsFontNotFoundInCache, + + &RendererTest::lineGlyphQuads2D, + &RendererTest::lineGlyphQuads2DArrayGlyphCache}); + + addInstancedTests({&RendererTest::alignLine}, + Containers::arraySize(AlignLineData)); + + addTests({&RendererTest::alignLineInvalidDirection}); + + addInstancedTests({&RendererTest::alignBlock}, + Containers::arraySize(AlignBlockData)); + + addTests({&RendererTest::alignBlockInvalidDirection, + + &RendererTest::glyphQuadIndices, + &RendererTest::glyphQuadIndices, + &RendererTest::glyphQuadIndices, + &RendererTest::glyphQuadIndicesTypeTooSmall}); + addInstancedTests({&RendererTest::renderData}, Containers::arraySize(RenderDataData)); @@ -271,6 +360,494 @@ DummyGlyphCache testGlyphCache(AbstractFont& font) { return cache; } +DummyGlyphCache testGlyphCacheArray(AbstractFont& font) { + DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20, 3}}; + + /* Add one more font to verify the right one gets picked */ + cache.addFont(96); + UnsignedInt fontId = cache.addFont(font.glyphCount(), &font); + + /* Three glyphs, covering bottom, top left and top right of the cache */ + cache.addGlyph(fontId, 3, {5, 10}, 2, {{}, {20, 10}}); + cache.addGlyph(fontId, 7, {10, 5}, 0, {{0, 10}, {10, 20}}); + cache.addGlyph(fontId, 9, {5, 5}, 1, {{10, 10}, {20, 20}}); + + return cache; +} + +void RendererTest::lineGlyphPositions() { + TestFont font; + font.openFile({}, 2.5f); + + Vector2 glyphOffsets[]{ + {0.2f, -0.4f}, + {0.4f, 0.8f}, + {-0.2f, 0.4f}, + }; + Vector2 glyphAdvances[]{ + {1.0f, 0.0f}, + {2.0f, 0.2f}, + {3.0f, -0.2f} + }; + Vector2 cursor{100.0f, 200.0f}; + + /* The font is opened at 2.5, rendering at 1.25, so everything will be + scaled by 0.5 */ + Vector2 glyphPositions[3]; + Range2D rectangle = renderLineGlyphPositionsInto(font, 1.25f, LayoutDirection::HorizontalTopToBottom, glyphOffsets, glyphAdvances, cursor, glyphPositions); + /* The rectangle contains the cursor range and descent to ascent */ + CORRADE_COMPARE(rectangle, (Range2D{{100.0f, 198.75f}, {103.0f, 202.25}})); + CORRADE_COMPARE(cursor, (Vector2{103.0f, 200.0f})); + CORRADE_COMPARE_AS(Containers::arrayView(glyphPositions), Containers::arrayView({ + {100.1f, 199.8f}, + {100.7f, 200.4f}, + {101.4f, 200.3f} + }), TestSuite::Compare::Container); +} + +void RendererTest::lineGlyphPositionsAliasedViews() { + /* Like lineGlyphPositions(), but with the input data stored in the output + array. The internals should be written in a way that doesn't overwrite + the input before it's read. */ + TestFont font; + font.openFile({}, 2.5f); + + Vector2 glyphOffsetsPositions[]{ + {0.2f, -0.4f}, + {0.4f, 0.8f}, + {-0.2f, 0.4f}, + }; + Vector2 glyphAdvances[]{ + {1.0f, 0.0f}, + {2.0f, 0.2f}, + {3.0f, -0.2f} + }; + Vector2 cursor{100.0f, 200.0f}; + + Range2D rectangle = renderLineGlyphPositionsInto(font, 1.25f, LayoutDirection::HorizontalTopToBottom, glyphOffsetsPositions, glyphAdvances, cursor, glyphOffsetsPositions); + CORRADE_COMPARE(rectangle, (Range2D{{100.0f, 198.75f}, {103.0f, 202.25}})); + CORRADE_COMPARE(cursor, (Vector2{103.0f, 200.0f})); + CORRADE_COMPARE_AS(Containers::arrayView(glyphOffsetsPositions), Containers::arrayView({ + {100.1f, 199.8f}, + {100.7f, 200.4f}, + {101.4f, 200.3f} + }), TestSuite::Compare::Container); +} + +void RendererTest::lineGlyphPositionsInvalidViewSizes() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + Vector2 data[5]; + Vector2 dataInvalid[4]; + Vector2 cursor; + + std::ostringstream out; + Error redirectError{&out}; + renderLineGlyphPositionsInto(font, 10.0f, LayoutDirection::HorizontalTopToBottom, data, data, cursor, dataInvalid); + renderLineGlyphPositionsInto(font, 10.0f, LayoutDirection::HorizontalTopToBottom, data, dataInvalid, cursor, data); + renderLineGlyphPositionsInto(font, 10.0f, LayoutDirection::HorizontalTopToBottom, dataInvalid, data, cursor, data); + CORRADE_COMPARE(out.str(), + "Text::renderLineGlyphPositionsInto(): expected glyphOffsets, glyphAdvances and output views to have the same size, got 5, 5 and 4\n" + "Text::renderLineGlyphPositionsInto(): expected glyphOffsets, glyphAdvances and output views to have the same size, got 5, 4 and 5\n" + "Text::renderLineGlyphPositionsInto(): expected glyphOffsets, glyphAdvances and output views to have the same size, got 4, 5 and 5\n"); +} + +void RendererTest::lineGlyphPositionsInvalidDirection() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + Vector2 cursor; + + std::ostringstream out; + Error redirectError{&out}; + renderLineGlyphPositionsInto(font, 10.0f, LayoutDirection::VerticalLeftToRight, {}, {}, cursor, {}); + CORRADE_COMPARE(out.str(), "Text::renderLineGlyphPositionsInto(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalLeftToRight\n"); +} + +void RendererTest::lineGlyphPositionsNoFontOpened() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + Vector2 cursor; + + std::ostringstream out; + Error redirectError{&out}; + renderLineGlyphPositionsInto(font, 10.0f, LayoutDirection::HorizontalTopToBottom, {}, {}, cursor, {}); + CORRADE_COMPARE(out.str(), "Text::renderLineGlyphPositionsInto(): no font opened\n"); +} + +void RendererTest::lineGlyphQuads() { + TestFont font; + font.openFile({}, 2.5f); + DummyGlyphCache cache = testGlyphCacheArray(font); + + Vector2 glyphPositions[]{ + {100.0f, 200.0f}, + {103.0f, 202.0f}, + {107.0f, 196.0f} + }; + UnsignedInt glyphIds[]{ + 3, 7, 9 + }; + + Vector2 positions[3*4]; + Vector3 textureCoordinates[3*4]; + /* The font is opened at 2.5, rendering at 1.25, so everything will be + scaled by 0.5 */ + Range2D rectangle = renderGlyphQuadsInto(font, 1.25f, cache, glyphPositions, glyphIds, positions, textureCoordinates); + CORRADE_COMPARE(rectangle, (Range2D{{102.5f, 198.5f}, {114.5f, 210.0f}})); + + /* 2---3 + | | + 0---1 */ + CORRADE_COMPARE_AS(Containers::arrayView(positions), Containers::arrayView({ + {102.5f, 205.0f}, /* Offset {5, 10}, size {20, 10}, scaled by 0.5 */ + {112.5f, 205.0f}, + {102.5f, 210.0f}, + {112.5f, 210.0f}, + + {108.0f, 204.5f}, /* Offset {10, 5}, size {10, 10}, scaled by 0.5 */ + {113.0f, 204.5f}, + {108.0f, 209.5f}, + {113.0f, 209.5f}, + + {109.5f, 198.5f}, /* Offset {5, 5}, size {10, 10}, scaled by 0.5 */ + {114.5f, 198.5f}, + {109.5f, 203.5f}, + {114.5f, 203.5f}, + }), TestSuite::Compare::Container); + + /* First glyph is bottom, second top left, third top right; layer is + different for each. + + +-+-+ + |b|c| + 2---3 + | a | + 0---1 */ + CORRADE_COMPARE_AS(Containers::arrayView(textureCoordinates), Containers::arrayView({ + {0.0f, 0.0f, 2.0f}, + {1.0f, 0.0f, 2.0f}, + {0.0f, 0.5f, 2.0f}, + {1.0f, 0.5f, 2.0f}, + + {0.0f, 0.5f, 0.0f}, + {0.5f, 0.5f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, 1.0f, 0.0f}, + + {0.5f, 0.5f, 1.0f}, + {1.0f, 0.5f, 1.0f}, + {0.5f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f}, + }), TestSuite::Compare::Container); +} + +void RendererTest::lineGlyphQuadsAliasedViews() { + /* Like lineGlyphPositions(), but with the input data stored in the output + array. The internals should be written in a way that doesn't overwrite + the input before it's read. */ + + TestFont font; + font.openFile({}, 2.5f); + DummyGlyphCache cache = testGlyphCacheArray(font); + + Vector2 positions[3*4]; + Vector3 textureCoordinates[3*4]; + + Containers::StridedArrayView1D glyphPositions = Containers::stridedArrayView(positions).every(4); + Utility::copy({ + {100.0f, 200.0f}, + {103.0f, 202.0f}, + {107.0f, 196.0f} + }, glyphPositions); + + Containers::StridedArrayView1D glyphIds = Containers::arrayCast(Containers::stridedArrayView(textureCoordinates).every(4)); + Utility::copy({ + 3, 7, 9 + }, glyphIds); + + Range2D rectangle = renderGlyphQuadsInto(font, 1.25f, cache, glyphPositions, glyphIds, positions, textureCoordinates); + CORRADE_COMPARE(rectangle, (Range2D{{102.5f, 198.5f}, {114.5f, 210.0f}})); + + CORRADE_COMPARE_AS(Containers::arrayView(positions), Containers::arrayView({ + {102.5f, 205.0f}, + {112.5f, 205.0f}, + {102.5f, 210.0f}, + {112.5f, 210.0f}, + + {108.0f, 204.5f}, + {113.0f, 204.5f}, + {108.0f, 209.5f}, + {113.0f, 209.5f}, + + {109.5f, 198.5f}, + {114.5f, 198.5f}, + {109.5f, 203.5f}, + {114.5f, 203.5f}, + }), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(Containers::arrayView(textureCoordinates), Containers::arrayView({ + {0.0f, 0.0f, 2.0f}, + {1.0f, 0.0f, 2.0f}, + {0.0f, 0.5f, 2.0f}, + {1.0f, 0.5f, 2.0f}, + + {0.0f, 0.5f, 0.0f}, + {0.5f, 0.5f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, 1.0f, 0.0f}, + + {0.5f, 0.5f, 1.0f}, + {1.0f, 0.5f, 1.0f}, + {0.5f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f}, + }), TestSuite::Compare::Container); +} + +void RendererTest::lineGlyphQuadsInvalidViewSizes() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}}; + Vector2 glyphPositions[4]; + Vector2 glyphPositionsInvalid[5]; + UnsignedInt glyphIds[4]; + UnsignedInt glyphIdsInvalid[3]; + Vector2 positions[16]; + Vector2 positionsInvalid[15]; + Vector3 textureCoordinates[16]; + Vector3 textureCoordinatesInvalid[17]; + + std::ostringstream out; + Error redirectError{&out}; + renderGlyphQuadsInto(font, 10.0f, cache, glyphPositions, glyphIdsInvalid, positions, textureCoordinates); + renderGlyphQuadsInto(font, 10.0f, cache, glyphPositionsInvalid, glyphIds, positions, textureCoordinates); + renderGlyphQuadsInto(font, 10.0f, cache, glyphPositions, glyphIds, positions, textureCoordinatesInvalid); + renderGlyphQuadsInto(font, 10.0f, cache, glyphPositions, glyphIds, positionsInvalid, textureCoordinates); + CORRADE_COMPARE_AS(out.str(), + "Text::renderGlyphQuadsInto(): expected glyphIds and glyphPositions views to have the same size, got 3 and 4\n" + "Text::renderGlyphQuadsInto(): expected glyphIds and glyphPositions views to have the same size, got 4 and 5\n" + "Text::renderGlyphQuadsInto(): expected vertexPositions and vertexTextureCoordinates views to have 16 elements, got 16 and 17\n" + "Text::renderGlyphQuadsInto(): expected vertexPositions and vertexTextureCoordinates views to have 16 elements, got 15 and 16\n", + TestSuite::Compare::String); +} + +void RendererTest::lineGlyphQuadsNoFontOpened() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}}; + + std::ostringstream out; + Error redirectError{&out}; + renderGlyphQuadsInto(font, 10.0f, cache, nullptr, nullptr, nullptr, Containers::StridedArrayView1D{}); + CORRADE_COMPARE(out.str(), "Text::renderGlyphQuadsInto(): no font opened\n"); +} + +void RendererTest::lineGlyphQuadsFontNotFoundInCache() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + font.openFile({}, 0.5f); + DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}}; + cache.addFont(56); + cache.addFont(13); + + std::ostringstream out; + Error redirectError{&out}; + renderGlyphQuadsInto(font, 10.0f, cache, nullptr, nullptr, nullptr, Containers::StridedArrayView1D{}); + CORRADE_COMPARE(out.str(), "Text::renderGlyphQuadsInto(): font not found among 2 fonts in passed glyph cache\n"); +} + +void RendererTest::lineGlyphQuads2D() { + /* Like lineGlyphPositions(), but with just a 2D glyph cache and using the + three-component overload. */ + + TestFont font; + font.openFile({}, 2.5f); + DummyGlyphCache cache = testGlyphCache(font); + + Vector2 glyphPositions[]{ + {100.0f, 200.0f}, + {103.0f, 202.0f}, + {107.0f, 196.0f} + }; + UnsignedInt glyphIds[]{ + 3, 7, 9 + }; + + Vector2 positions[3*4]; + Vector2 textureCoordinates[3*4]; + Range2D rectangle = renderGlyphQuadsInto(font, 1.25f, cache, glyphPositions, glyphIds, positions, textureCoordinates); + CORRADE_COMPARE(rectangle, (Range2D{{102.5f, 198.5f}, {114.5f, 210.0f}})); + + CORRADE_COMPARE_AS(Containers::arrayView(positions), Containers::arrayView({ + {102.5f, 205.0f}, + {112.5f, 205.0f}, + {102.5f, 210.0f}, + {112.5f, 210.0f}, + + {108.0f, 204.5f}, + {113.0f, 204.5f}, + {108.0f, 209.5f}, + {113.0f, 209.5f}, + + {109.5f, 198.5f}, + {114.5f, 198.5f}, + {109.5f, 203.5f}, + {114.5f, 203.5f}, + }), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(Containers::arrayView(textureCoordinates), Containers::arrayView({ + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {0.0f, 0.5f}, + {1.0f, 0.5f}, + + {0.0f, 0.5f}, + {0.5f, 0.5f}, + {0.0f, 1.0f}, + {0.5f, 1.0f}, + + {0.5f, 0.5f}, + {1.0f, 0.5f}, + {0.5f, 1.0f}, + {1.0f, 1.0f}, + }), TestSuite::Compare::Container); +} + +void RendererTest::lineGlyphQuads2DArrayGlyphCache() { + CORRADE_SKIP_IF_NO_ASSERT(); + + TestFont font; + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } cache{PixelFormat::R8Unorm, {20, 20, 2}}; + + std::ostringstream out; + Error redirectError{&out}; + renderGlyphQuadsInto(font, 10.0f, cache, nullptr, nullptr, nullptr, Containers::StridedArrayView1D{}); + CORRADE_COMPARE(out.str(), "Text::renderGlyphQuadsInto(): can't use this overload with an array glyph cache\n"); +} + +void RendererTest::alignLine() { + auto&& data = AlignLineData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Range2D rectangle{{10.0f, 200.0f}, {13.5f, -960.0f}}; + + /* The positions aren't taken into account, so they can be arbitrary */ + Vector2 positions[]{ + {100.0f, 200.0f}, + {300.0f, -60.0f}, + {-10.0f, 100.0f}, + }; + Range2D alignedRectangle = alignRenderedLine(rectangle, LayoutDirection::HorizontalTopToBottom, data.alignment, positions); + CORRADE_COMPARE(alignedRectangle, rectangle.translated({data.offset, 0.0f})); + CORRADE_COMPARE_AS(Containers::arrayView(positions), Containers::arrayView({ + {100.0f + data.offset, 200.0f}, + {300.0f + data.offset, -60.0f}, + {-10.0f + data.offset, 100.0f} + }), TestSuite::Compare::Container); +} + +void RendererTest::alignLineInvalidDirection() { + CORRADE_SKIP_IF_NO_ASSERT(); + + std::ostringstream out; + Error redirectError{&out}; + alignRenderedLine({}, LayoutDirection::VerticalRightToLeft, Alignment::LineLeft, nullptr); + CORRADE_COMPARE(out.str(), "Text::alignRenderedLine(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n"); +} + +void RendererTest::alignBlock() { + auto&& data = AlignBlockData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Range2D rectangle{{100.0f, 9.5f}, {-70.0f, 19.5f}}; + + /* The positions aren't taken into account, so they can be arbitrary */ + Vector2 positions[]{ + {100.0f, 200.0f}, + {-10.0f, 100.0f}, + {300.0f, -60.0f}, + }; + Range2D alignedRectangle = alignRenderedBlock(rectangle, LayoutDirection::HorizontalTopToBottom, data.alignment, positions); + CORRADE_COMPARE(alignedRectangle, rectangle.translated({0.0f, data.offset})); + CORRADE_COMPARE_AS(Containers::arrayView(positions), Containers::arrayView({ + {100.0f, 200.0f + data.offset}, + {-10.0f, 100.0f + data.offset}, + {300.0f, -60.0f + data.offset}, + }), TestSuite::Compare::Container); +} + +void RendererTest::alignBlockInvalidDirection() { + CORRADE_SKIP_IF_NO_ASSERT(); + + std::ostringstream out; + Error redirectError{&out}; + alignRenderedBlock({}, LayoutDirection::VerticalRightToLeft, Alignment::LineLeft, nullptr); + CORRADE_COMPARE(out.str(), "Text::alignRenderedBlock(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalRightToLeft\n"); +} + +template void RendererTest::glyphQuadIndices() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + /* 2---3 2 3---5 + | | |\ \ | + | | | \ \ | + | | | \ \| + 0---1 0---1 4 */ + T indices[3*6]; + renderGlyphQuadIndicesInto(60, indices); + CORRADE_COMPARE_AS(Containers::arrayView(indices), Containers::arrayView({ + 240, 241, 242, 242, 241, 243, + 244, 245, 246, 246, 245, 247, + 248, 249, 250, 250, 249, 251 + }), TestSuite::Compare::Container); +} + +void RendererTest::glyphQuadIndicesTypeTooSmall() { + CORRADE_SKIP_IF_NO_ASSERT(); + + /* This should be fine */ + UnsignedByte indices8[18]; + UnsignedShort indices16[18]; + UnsignedInt indices32[18]; + renderGlyphQuadIndicesInto(256/4 - 3, indices8); + renderGlyphQuadIndicesInto(65536/4 - 3, indices16); + renderGlyphQuadIndicesInto(4294967296u/4 - 3, indices32); + CORRADE_COMPARE(indices8[17], 255); + CORRADE_COMPARE(indices16[17], 65535); + CORRADE_COMPARE(indices32[17], 4294967295); + + /* Empty view also */ + renderGlyphQuadIndicesInto(256/4, Containers::ArrayView{}); + renderGlyphQuadIndicesInto(65536/4, Containers::ArrayView{}); + renderGlyphQuadIndicesInto(4294967296u/4, Containers::ArrayView{}); + + std::ostringstream out; + Error redirectError{&out}; + renderGlyphQuadIndicesInto(256/4 - 3 + 1, indices8); + renderGlyphQuadIndicesInto(65536/4 - 3 + 1, indices16); + renderGlyphQuadIndicesInto(4294967296u/4 - 3 + 1, indices32); + /* Should assert even if there's actually no indices to write */ + renderGlyphQuadIndicesInto(256/4 + 1, Containers::ArrayView{}); + renderGlyphQuadIndicesInto(65536/4 + 1, Containers::ArrayView{}); + renderGlyphQuadIndicesInto(4294967296u/4 + 1, Containers::ArrayView{}); + CORRADE_COMPARE(out.str(), + "Text::renderGlyphQuadIndicesInto(): max index value of 259 cannot fit into a 8-bit type\n" + "Text::renderGlyphQuadIndicesInto(): max index value of 65539 cannot fit into a 16-bit type\n" + "Text::renderGlyphQuadIndicesInto(): max index value of 4294967299 cannot fit into a 32-bit type\n" + "Text::renderGlyphQuadIndicesInto(): max index value of 259 cannot fit into a 8-bit type\n" + "Text::renderGlyphQuadIndicesInto(): max index value of 65539 cannot fit into a 16-bit type\n" + "Text::renderGlyphQuadIndicesInto(): max index value of 4294967299 cannot fit into a 32-bit type\n"); +} + void RendererTest::renderData() { auto&& data = RenderDataData[testCaseInstanceId()]; setTestCaseDescription(data.name); @@ -301,32 +878,32 @@ void RendererTest::renderData() { +-+ +-+ |c| - 0---2 |b| +-+ + 2---3 |b| +-+ | a | +-+ - 1---3 */ + 0---1 */ CORRADE_COMPARE_AS(positions, (std::vector{ /* Cursor is {0, 0}. Offset from the cache is {5, 10}, offset from the renderer is {0, 1}, size is {20, 10}; all scaled by 0.5 */ - Vector2{ 2.5f, 10.5f} + data.offset, Vector2{ 2.5f, 5.5f} + data.offset, - Vector2{12.5f, 10.5f} + data.offset, Vector2{12.5f, 5.5f} + data.offset, + Vector2{ 2.5f, 10.5f} + data.offset, + Vector2{12.5f, 10.5f} + data.offset, /* Advance was {1, 0.5}, cursor is {1, 0.5}. Offset from the cache is {10, 5}, offset from the renderer is {0, 2}, size is {10, 10}; all scaled by 0.5 */ - Vector2{ 5.5f, 8.75f} + data.offset, Vector2{ 5.5f, 3.75f} + data.offset, - Vector2{10.5f, 8.75f} + data.offset, Vector2{10.5f, 3.75f} + data.offset, + Vector2{ 5.5f, 8.75f} + data.offset, + Vector2{10.5f, 8.75f} + data.offset, /* Advance was {2, -0.5}, cursor is {3, 0}. Offset from the cache is {5, 5}, offset from the renderer is {0, 3}, size is {10, 10}; all scaled by 0.5 */ - Vector2{ 4.0f, 9.0f} + data.offset, Vector2{ 4.0f, 4.0f} + data.offset, + Vector2{ 9.0f, 4.0f} + data.offset, + Vector2{ 4.0f, 9.0f} + data.offset, Vector2{ 9.0f, 9.0f} + data.offset, - Vector2{ 9.0f, 4.0f} + data.offset }), TestSuite::Compare::Container); /* Bounds. Different depending on whether or not GlyphBounds alignment is @@ -340,36 +917,36 @@ void RendererTest::renderData() { right. +-+-+ |b|c| - 0---2 + 2---3 | a | - 1---3 */ + 0---1 */ CORRADE_COMPARE_AS(textureCoordinates, (std::vector{ - {0.0f, 0.5f}, {0.0f, 0.0f}, - {1.0f, 0.5f}, {1.0f, 0.0f}, + {0.0f, 0.5f}, + {1.0f, 0.5f}, - {0.0f, 1.0f}, {0.0f, 0.5f}, - {0.5f, 1.0f}, {0.5f, 0.5f}, - + {0.0f, 1.0f}, {0.5f, 1.0f}, + {0.5f, 0.5f}, + {1.0f, 0.5f}, + {0.5f, 1.0f}, {1.0f, 1.0f}, - {1.0f, 0.5f} }), TestSuite::Compare::Container); /* Indices - 0---2 0---2 5 - | | | / /| - | | | / / | - | | |/ / | - 1---3 1 3---4 */ + 2---3 2 3---5 + | | |\ \ | + | | | \ \ | + | | | \ \| + 0---1 0---1 4 */ CORRADE_COMPARE_AS(indices, (std::vector{ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10 + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, }), TestSuite::Compare::Container); } @@ -449,70 +1026,70 @@ void RendererTest::multiline() { [g] [h] [i] */ CORRADE_COMPARE_AS(positions, (std::vector{ - Vector2{0.0f, 1.0f} + data.offset0, /* a */ - Vector2{0.0f, 0.0f} + data.offset0, - Vector2{1.0f, 1.0f} + data.offset0, + Vector2{0.0f, 0.0f} + data.offset0, /* a */ Vector2{1.0f, 0.0f} + data.offset0, + Vector2{0.0f, 1.0f} + data.offset0, + Vector2{1.0f, 1.0f} + data.offset0, - Vector2{2.0f, 1.0f} + data.offset0, /* b */ - Vector2{2.0f, 0.0f} + data.offset0, - Vector2{3.0f, 1.0f} + data.offset0, + Vector2{2.0f, 0.0f} + data.offset0, /* b */ Vector2{3.0f, 0.0f} + data.offset0, + Vector2{2.0f, 1.0f} + data.offset0, + Vector2{3.0f, 1.0f} + data.offset0, - Vector2{4.0f, 1.0f} + data.offset0, /* c */ - Vector2{4.0f, 0.0f} + data.offset0, - Vector2{5.0f, 1.0f} + data.offset0, + Vector2{4.0f, 0.0f} + data.offset0, /* c */ Vector2{5.0f, 0.0f} + data.offset0, + Vector2{4.0f, 1.0f} + data.offset0, + Vector2{5.0f, 1.0f} + data.offset0, - Vector2{6.0f, 1.0f} + data.offset0, /* d */ - Vector2{6.0f, 0.0f} + data.offset0, - Vector2{7.0f, 1.0f} + data.offset0, + Vector2{6.0f, 0.0f} + data.offset0, /* d */ Vector2{7.0f, 0.0f} + data.offset0, + Vector2{6.0f, 1.0f} + data.offset0, + Vector2{7.0f, 1.0f} + data.offset0, - Vector2{0.0f, 1.0f} + data.offset1, /* e */ - Vector2{0.0f, 0.0f} + data.offset1, - Vector2{1.0f, 1.0f} + data.offset1, + Vector2{0.0f, 0.0f} + data.offset1, /* e */ Vector2{1.0f, 0.0f} + data.offset1, + Vector2{0.0f, 1.0f} + data.offset1, + Vector2{1.0f, 1.0f} + data.offset1, - Vector2{2.0f, 1.0f} + data.offset1, /* f */ - Vector2{2.0f, 0.0f} + data.offset1, - Vector2{3.0f, 1.0f} + data.offset1, + Vector2{2.0f, 0.0f} + data.offset1, /* f */ Vector2{3.0f, 0.0f} + data.offset1, + Vector2{2.0f, 1.0f} + data.offset1, + Vector2{3.0f, 1.0f} + data.offset1, /* Two linebreaks here */ - Vector2{0.0f, 1.0f} + data.offset2, /* g */ - Vector2{0.0f, 0.0f} + data.offset2, - Vector2{1.0f, 1.0f} + data.offset2, + Vector2{0.0f, 0.0f} + data.offset2, /* g */ Vector2{1.0f, 0.0f} + data.offset2, + Vector2{0.0f, 1.0f} + data.offset2, + Vector2{1.0f, 1.0f} + data.offset2, - Vector2{2.0f, 1.0f} + data.offset2, /* h */ - Vector2{2.0f, 0.0f} + data.offset2, - Vector2{3.0f, 1.0f} + data.offset2, + Vector2{2.0f, 0.0f} + data.offset2, /* h */ Vector2{3.0f, 0.0f} + data.offset2, + Vector2{2.0f, 1.0f} + data.offset2, + Vector2{3.0f, 1.0f} + data.offset2, - Vector2{4.0f, 1.0f} + data.offset2, /* i */ - Vector2{4.0f, 0.0f} + data.offset2, - Vector2{5.0f, 1.0f} + data.offset2, + Vector2{4.0f, 0.0f} + data.offset2, /* i */ Vector2{5.0f, 0.0f} + data.offset2, + Vector2{4.0f, 1.0f} + data.offset2, + Vector2{5.0f, 1.0f} + data.offset2, }), TestSuite::Compare::Container); /* Indices - 0---2 0---2 5 - | | | / /| - | | | / / | - | | |/ / | - 1---3 1 3---4 */ + 2---3 2 3---5 + | | |\ \ | + | | | \ \ | + | | | \ \| + 0---1 0---1 4 */ CORRADE_COMPARE_AS(indices, (std::vector{ - 0, 1, 2, 1, 3, 2, - 4, 5, 6, 5, 7, 6, - 8, 9, 10, 9, 11, 10, - 12, 13, 14, 13, 15, 14, - 16, 17, 18, 17, 19, 18, - 20, 21, 22, 21, 23, 22, - 24, 25, 26, 25, 27, 26, - 28, 29, 30, 29, 31, 30, - 32, 33, 34, 33, 35, 34 + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19, + 20, 21, 22, 22, 21, 23, + 24, 25, 26, 26, 25, 27, + 28, 29, 30, 30, 29, 31, + 32, 33, 34, 34, 33, 35, }), TestSuite::Compare::Container); }