#include "CmTextSprite.h" #include "CmDebug.h" #include "CmFontDesc.h" #include "CmFont.h" #include "CmVector2.h" #include "CmMath.h" #include "CmGUIMaterialManager.h" namespace CamelotEngine { const int SPACE_CHAR = 32; class TextWord { public: TextWord(bool spacer) :mWidth(0), mSpacer(spacer), mSpaceWidth(0) { } void addChar(const CHAR_DESC& desc) { mChars.push_back(desc); calculateWidth(); } void addSpace(UINT32 spaceWidth) { mSpaceWidth += spaceWidth; calculateWidth(); } void removeLastChar() { if(mChars.size() > 0) { mChars.erase(mChars.end() - 1); calculateWidth(); } } UINT32 getWidth() const { return mWidth; } bool isSpacer() const { return mSpacer; } const vector::type& getChars() const { return mChars; } private: vector::type mChars; UINT32 mWidth; bool mSpacer; UINT32 mSpaceWidth; void calculateWidth() { if(isSpacer()) { mWidth = mSpaceWidth; return; } if(mChars.size() == 0) { mWidth = 0; return; } mWidth = 0; UINT32 kerning = 0; for(size_t i = 0; i < mChars.size() - 1; i++) { mWidth += mChars[i].xAdvance + kerning; kerning = 0; for(size_t j = 0; j < mChars[i].kerningPairs.size(); j++) { if(mChars[i].kerningPairs[j].otherCharId == mChars[i + 1].charId) { kerning = mChars[i].kerningPairs[j].amount; break; } } } mWidth += mChars[mChars.size() - 1].xAdvance + kerning; } }; class TextLine { public: TextLine() :mWidth(0), mLastWord(nullptr) { } ~TextLine() { for(auto iter = mWords.begin(); iter != mWords.end(); ++iter) CM_DELETE(*iter, TextWord, ScratchAlloc); } void add(const CHAR_DESC& charDesc) { if(mLastWord == nullptr) { TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(false); mLastWord = newWord; mWords.push_back(mLastWord); mLastWord->addChar(charDesc); } else { if(mLastWord->isSpacer()) { TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(false); mLastWord = newWord; mWords.push_back(mLastWord); } mLastWord->addChar(charDesc); } calculateBounds(); } void addSpace(UINT32 spaceWidth) { if(mLastWord == nullptr) { TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(true); mLastWord = newWord; mWords.push_back(mLastWord); mLastWord->addSpace(spaceWidth); } else { TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(true); // Each space is counted as its own word, to make certain operations easier mLastWord = newWord; mWords.push_back(mLastWord); mLastWord->addSpace(spaceWidth); } calculateBounds(); } void addWord(TextWord* word) { mWords.push_back(word); mLastWord = word; calculateBounds(); } TextWord* removeLastWord() { if(mWords.size() == 0) return nullptr; TextWord* word = mWords[mWords.size() - 1]; mWords.erase(mWords.end() - 1); if(mWords.size() > 0) mLastWord = mWords[mWords.size() - 1]; else mLastWord = nullptr; calculateBounds(); return word; } UINT32 getWidth() const { return mWidth; } Point getPosition() const { return mPosition; } void setPosition(const Point& pos) { mPosition = pos; } void fillBuffer(const vector::type& renderElements, vector::type& faceOffsets, const FontData& fontData) { UINT32 penX = mPosition.x; UINT32 baselineY = mPosition.y + fontData.fontDesc.baselineOffset; for(auto wordIter = mWords.begin(); wordIter != mWords.end(); ++wordIter) { if((*wordIter)->isSpacer()) { penX += fontData.fontDesc.spaceWidth; } else { const vector::type& chars = (*wordIter)->getChars(); UINT32 kerning = 0; for(auto charIter = chars.begin(); charIter != chars.end(); ++charIter) { INT32 curX = penX + charIter->xOffset; INT32 curY = -((INT32)baselineY - charIter->yOffset); UINT32 curVert = faceOffsets[charIter->page] * 4; UINT32 curIndex = faceOffsets[charIter->page] * 6; const SpriteRenderElement& renderElem = renderElements[charIter->page]; renderElem.vertices[curVert + 0] = Vector2((float)curX, (float)curY); renderElem.vertices[curVert + 1] = Vector2((float)(curX + charIter->width), (float)curY); renderElem.vertices[curVert + 2] = Vector2((float)curX, (float)curY - (float)charIter->height); renderElem.vertices[curVert + 3] = Vector2((float)(curX + charIter->width), (float)curY - (float)charIter->height); renderElem.uvs[curVert + 0] = Vector2(charIter->uvX, charIter->uvY); renderElem.uvs[curVert + 1] = Vector2(charIter->uvX + charIter->uvWidth, charIter->uvY); renderElem.uvs[curVert + 2] = Vector2(charIter->uvX, charIter->uvY + charIter->uvHeight); renderElem.uvs[curVert + 3] = Vector2(charIter->uvX + charIter->uvWidth, charIter->uvY + charIter->uvHeight); renderElem.indexes[curIndex + 0] = curVert + 0; renderElem.indexes[curIndex + 1] = curVert + 1; renderElem.indexes[curIndex + 2] = curVert + 2; renderElem.indexes[curIndex + 3] = curVert + 1; renderElem.indexes[curIndex + 4] = curVert + 3; renderElem.indexes[curIndex + 5] = curVert + 2; penX += charIter->xAdvance + kerning; faceOffsets[charIter->page]++; kerning = 0; if((charIter + 1) != chars.end()) { for(size_t j = 0; j < charIter->kerningPairs.size(); j++) { if(charIter->kerningPairs[j].otherCharId == (charIter + 1)->charId) { kerning = charIter->kerningPairs[j].amount; break; } } } } } } } private: UINT32 mWidth; vector::type mWords; TextWord* mLastWord; Point mPosition; void calculateBounds() { mWidth = 0; for(auto iter = mWords.begin(); iter != mWords.end(); ++iter) { mWidth += (*iter)->getWidth(); } } }; TextSprite::TextSprite() :mFontSize(0), mWordWrap(false), mHorzAlign(THA_Left), mVertAlign(TVA_Top) { } TextSprite::TextSprite(const String& text, HFont font, UINT32 fontSize) :mText(text), mFont(font), mFontSize(fontSize), mWordWrap(false), mHorzAlign(THA_Left), mVertAlign(TVA_Top) { } void TextSprite::updateMesh() const { const FontData* fontData = getFontData(); if(fontData == nullptr) { clearMesh(); return; } if(fontData->size != mFontSize) { LOGWRN("Unable to find font with specified size (" + toString(mFontSize) + "). Using nearest available size: " + toString(fontData->size)); } bool heightIsLimited = mHeight > 0; bool widthIsLimited = mWidth > 0; TextLine* curLine = CM_NEW(TextLine, ScratchAlloc) TextLine(); vector::type textLines; textLines.push_back(curLine); UINT32 curHeight = fontData->fontDesc.lineHeight; UINT32 charIdx = 0; vector::type newRenderElemSizes; while(true) { if(charIdx >= mText.size()) break; if(mText[charIdx] == '\n') { if(heightIsLimited && (curHeight + fontData->fontDesc.lineHeight * 2) > mHeight) break; // Max height reached curLine = CM_NEW(TextLine, ScratchAlloc) TextLine(); textLines.push_back(curLine); curHeight += fontData->fontDesc.lineHeight; charIdx++; continue; } UINT32 charId = mText[charIdx]; const CHAR_DESC& charDesc = fontData->getCharDesc(charId); if(charId != SPACE_CHAR) { curLine->add(charDesc); if(charDesc.page >= (UINT32)newRenderElemSizes.size()) newRenderElemSizes.resize(charDesc.page + 1); newRenderElemSizes[charDesc.page]++; } else curLine->addSpace(fontData->fontDesc.spaceWidth); if(widthIsLimited && curLine->getWidth() > mWidth) { if(mWordWrap) { TextWord* lastWord = curLine->removeLastWord(); if(lastWord->isSpacer()) curLine->addWord(lastWord); // Spaces can stay on previous line even if they don't technically fit // No more lines fit vertically so we're done if(heightIsLimited && curHeight > mHeight) break; curLine = CM_NEW(TextLine, ScratchAlloc) TextLine(); textLines.push_back(curLine); curHeight += fontData->fontDesc.lineHeight; if(!lastWord->isSpacer()) curLine->addWord(lastWord); } else { // Nothing else we can do, chars don't fit so we are done break; } } charIdx++; } // Calc vertical alignment offset UINT32 vertDiff = std::max(0U, mHeight - curHeight); UINT32 vertOffset = 0; switch(mVertAlign) { case TVA_Top: vertOffset = 0; break; case TVA_Bottom: vertOffset = std::max(0, (INT32)(mHeight - curHeight)); break; case TVA_Center: vertOffset = std::max(0, (INT32)(mHeight - curHeight)) / 2; break; } // Calc horizontal alignment offset and set final line positions Point offset = mOffset + getAnchorOffset(); UINT32 curY = 0; for(size_t i = 0; i < textLines.size(); i++) { UINT32 horzOffset = 0; switch(mHorzAlign) { case THA_Left: horzOffset = 0; break; case THA_Right: horzOffset = std::max(0, (INT32)(mWidth - textLines[i]->getWidth())); break; case THA_Center: horzOffset = std::max(0, (INT32)(mWidth - textLines[i]->getWidth())) / 2; break; } textLines[i]->setPosition(offset + Point(horzOffset, vertOffset + curY)); curY += fontData->fontDesc.lineHeight; } // Actually generate a mesh if(mCachedRenderElements.size() < newRenderElemSizes.size()) mCachedRenderElements.resize(newRenderElemSizes.size()); UINT32 texPage = 0; for(auto& cachedElem : mCachedRenderElements) { UINT32 newNumQuads = newRenderElemSizes[texPage]; if(newNumQuads != cachedElem.numQuads) { UINT32 oldVertexCount = cachedElem.numQuads * 4; UINT32 oldIndexCount = cachedElem.numQuads * 6; if(cachedElem.vertices != nullptr) CM_DELETE_ARRAY(cachedElem.vertices, Vector2, oldVertexCount, ScratchAlloc); if(cachedElem.uvs != nullptr) CM_DELETE_ARRAY(cachedElem.uvs, Vector2, oldVertexCount, ScratchAlloc); if(cachedElem.indexes != nullptr) CM_DELETE_ARRAY(cachedElem.indexes, UINT32, oldIndexCount, ScratchAlloc); cachedElem.vertices = CM_NEW_ARRAY(Vector2, newNumQuads * 4, ScratchAlloc); cachedElem.uvs = CM_NEW_ARRAY(Vector2, newNumQuads * 4, ScratchAlloc); cachedElem.indexes = CM_NEW_ARRAY(UINT32, newNumQuads * 6, ScratchAlloc); cachedElem.numQuads = newNumQuads; } HMaterial newMaterial = GUIMaterialManager::instance().requestTextMaterial(fontData->texturePages[texPage]); if(cachedElem.material != nullptr) GUIMaterialManager::instance().releaseMaterial(newMaterial); cachedElem.material = newMaterial; texPage++; } vector::type faceOffsets(mCachedRenderElements.size(), 0); for(size_t i = 0; i < textLines.size(); i++) textLines[i]->fillBuffer(mCachedRenderElements, faceOffsets, *fontData); for(size_t i = 0; i < textLines.size(); i++) CM_DELETE(textLines[i], TextLine, ScratchAlloc); if(isClipRectangleValid()) { for(auto& renderElem : mCachedRenderElements) { clipToRect(renderElem.vertices, renderElem.uvs, renderElem.numQuads, mClipRect); } } } const FontData* TextSprite::getFontData() const { if(mFont == nullptr) return nullptr; UINT32 nearestSize = mFont->getClosestAvailableSize(mFontSize); return mFont->getFontDataForSize(nearestSize); } // This will only properly clip an array of rectangular quads // Vertices in the quad must be in a specific order: top left, top right, bottom left, bottom right // (0, 0) represents top left of the screen void TextSprite::clipToRect(Vector2* vertices, Vector2* uv, UINT32 numQuads, Rect clipRect) const { float left = (float)clipRect.x; float right = (float)clipRect.x + clipRect.width; float top = (float)clipRect.y; float bottom = (float)clipRect.y - clipRect.height; for(UINT32 i = 0; i < numQuads; i++) { UINT32 vertIdx = i * 4; // Attempt to skip those that are definitely not clipped if(vertices[vertIdx + 0].x >= left && vertices[vertIdx + 1].x <= right && vertices[vertIdx + 0].y <= top && vertices[vertIdx + 2].y >= bottom) { continue; } float du = (uv[vertIdx + 1].x - uv[vertIdx + 0].x) / (vertices[vertIdx + 1].x - vertices[vertIdx + 0].x); float dv = (uv[vertIdx + 0].y - uv[vertIdx + 2].y) / (vertices[vertIdx + 0].y - vertices[vertIdx + 2].y); // Clip left float newLeft = Math::Clamp(vertices[vertIdx + 0].x, left, right); float uvLeftOffset = (newLeft - vertices[vertIdx + 0].x) * du; vertices[vertIdx + 0].x = newLeft; vertices[vertIdx + 2].x = newLeft; uv[vertIdx + 0].x += uvLeftOffset; uv[vertIdx + 2].x += uvLeftOffset; // Clip right float newRight = Math::Clamp(vertices[vertIdx + 1].x, left, right); float uvRightOffset = (vertices[vertIdx + 1].x - newRight) * du; vertices[vertIdx + 1].x = newRight; vertices[vertIdx + 3].x = newRight; uv[vertIdx + 1].x -= uvRightOffset; uv[vertIdx + 3].x -= uvRightOffset; // Clip top float newTop = Math::Clamp(vertices[vertIdx + 0].y, bottom, top); float uvTopOffset = (vertices[vertIdx + 0].y - newTop) * dv; vertices[vertIdx + 0].y = newTop; vertices[vertIdx + 1].y = newTop; uv[vertIdx + 0].y -= uvTopOffset; uv[vertIdx + 1].y -= uvTopOffset; // Clip bottom float newBottom = Math::Clamp(vertices[vertIdx + 2].y, bottom, top); float uvBottomOffset = (newBottom - vertices[vertIdx + 2].y) * dv; vertices[vertIdx + 2].y = newBottom; vertices[vertIdx + 3].y = newBottom; uv[vertIdx + 2].y += uvBottomOffset; uv[vertIdx + 3].y += uvBottomOffset; } } }