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); }