1 /** 2 Copyright: Copyright (c) 2017-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module voxelman.graphics.font.textmesher; 7 8 import voxelman.math; 9 import voxelman.graphics; 10 import voxelman.graphics.font.font; 11 12 /// Output range for use with formattedWrite, etc 13 struct TextMesherSink 14 { 15 TextMesherParams* params; 16 17 void put(Range)(Range chars) 18 { 19 meshText(*params, chars); 20 } 21 } 22 23 /// Sink for glyps that uses TexturedBatch2d internally 24 struct TextRectSink 25 { 26 TexturedBatch2d* sink; 27 Texture texture; 28 size_t numRectsPutted; 29 30 void putRect(frect target, frect source, float depth, Color4ub color) 31 { 32 ++numRectsPutted; 33 sink.putRect(target, source, depth, color, texture); 34 } 35 36 void applyOffset(vec2 offset) 37 { 38 sink.addOffsetToLastRects(offset, numRectsPutted); 39 } 40 } 41 42 struct TextMesherParams 43 { 44 TextRectSink sink; // ref 45 FontRef font; 46 ivec2 origin = ivec2(0, 0); 47 ivec2 cursor = ivec2(0, 0); // ref 48 irect scissors = irect(-int.max/2, -int.max/2, int.max, int.max); 49 float depth = 0; 50 Color4ub color = Colors.black; 51 int scale = 1; 52 int tabSize = 4; 53 bool monospaced = false; 54 TextStyle[] styles; 55 int column; // ref 56 57 // ref 58 vec2 size = vec2(0, 0); // size of text relative to origin 59 } 60 61 struct TextStyle 62 { 63 Color4ub color = Colors.black; 64 } 65 66 alias StyleId = ubyte; 67 68 /// Modifies params.cursor and params.sink 69 void meshText(bool mesh = true, P, T)(ref P params, T textRange) 70 { 71 import std.range : repeat; 72 TextStyle[1] styles = [TextStyle(params.color)]; 73 auto prevStyles = params.styles; 74 params.styles = styles[]; 75 meshText!(mesh)(params, textRange, repeat(StyleId(0))); 76 params.styles = prevStyles; 77 } 78 79 struct StyledCodePoint 80 { 81 dchar codepoint; 82 TextStyle style; 83 } 84 85 struct TextStyleZip(T, S) 86 { 87 T textRange; 88 S styleRange; 89 90 bool empty() 91 { 92 return textRange.empty || styleRange.empty; 93 } 94 95 StyledCodePoint front() 96 { 97 return StyledCodePoint(textRange.front, styleRange.front); 98 } 99 100 void popFront() 101 { 102 textRange.popFront; 103 styleRange.popFront; 104 } 105 } 106 107 /// ditto 108 void meshText(bool mesh = true, P, T, S)(ref P params, T textRange, S styleRange) 109 { 110 import std.uni; 111 import std.utf; 112 113 void meshGlyph(Glyph* glyph) 114 { 115 int w = glyph.metrics.width; 116 int h = glyph.metrics.height; 117 118 int offsetX = params.monospaced ? glyph.metrics.offsetX : 0; 119 int x = params.origin.x + params.cursor.x + offsetX * params.scale; 120 int y = params.origin.y + params.cursor.y + (params.font.metrics.ascent - glyph.metrics.offsetY) * params.scale; 121 122 auto geometryRect = frect(x, y, w * params.scale, h * params.scale); 123 auto atlasRect = frect(glyph.atlasPosition.x, glyph.atlasPosition.y, w, h); 124 125 bool shouldDraw = clipTexturedRect(geometryRect, atlasRect, params.scissors); 126 127 if (shouldDraw) 128 { 129 TextStyle style = params.styles[styleRange.front]; 130 params.sink.putRect(geometryRect, atlasRect, params.depth, style.color);//params.color); 131 } 132 } 133 134 void updateMaxWidth() { 135 params.size.x = max(params.size.x, params.cursor.x); 136 } 137 138 int singleGlyphWidth = params.font.metrics.width * params.scale; 139 int singleGlyphHeight = params.font.metrics.height * params.scale; 140 int advanceX = params.font.metrics.advanceX * params.scale; 141 int advanceY = params.font.metrics.advanceY * params.scale; 142 143 foreach(dchar codePoint; textRange.byDchar) 144 { 145 switch(codePoint) // special chars 146 { 147 case ' ': 148 params.cursor.x += advanceX; 149 ++params.column; 150 continue; 151 case '\t': 152 int tabGlyphs = tabWidth(params.tabSize, params.column); 153 params.cursor.x += advanceX * tabGlyphs; 154 params.column += tabGlyphs; 155 continue; 156 case '\n': 157 updateMaxWidth(); 158 params.cursor.x = 0; 159 params.cursor.y += advanceY; 160 params.column = 0; 161 continue; 162 case '\r': 163 continue; 164 default: 165 ++params.column; 166 break; 167 } 168 169 Glyph* glyph = params.font.getGlyph(codePoint); 170 static if (mesh) meshGlyph(glyph); 171 172 int glyphAdvanceX = params.monospaced ? advanceX : glyph.metrics.advanceX; 173 params.cursor.x += glyphAdvanceX * params.scale; 174 static if (mesh) styleRange.popFront; 175 } 176 177 params.size.y = max(params.size.y, params.cursor.y + singleGlyphHeight); 178 updateMaxWidth(); 179 } 180 181 /// Changes params size and cursor 182 void measureText(P, T)(ref P params, T textRange) 183 { 184 meshText!(false)(params, textRange); 185 } 186 187 int tabWidth(int tabSize, int column) 188 { 189 return tabSize - column % tabSize; 190 } 191 192 /// Applies offset to all previously meshed glyphs 193 /// Number of meshed glyphs is taken from TextRectSink 194 void alignMeshedText(P)(ref P params, Alignment halign = Alignment.min, Alignment valign = Alignment.min, ivec2 area = ivec2(0,0)) 195 { 196 if (halign == Alignment.min && valign == Alignment.min) return; 197 198 ivec2 alignmentOffset = rectAlignmentOffset(ivec2(params.size), halign, valign, area); 199 params.sink.applyOffset(vec2(alignmentOffset)); 200 params.origin += alignmentOffset; 201 } 202 203 // modifies cursor to be aligned for passed text 204 void meshTextAligned(P, T)(ref P params, T textRange, Alignment halign = Alignment.min, Alignment valign = Alignment.min, ivec2 area = ivec2(0,0)) 205 { 206 if (halign == Alignment.min && valign == Alignment.min) { 207 meshText!(true)(params, textRange); 208 return; 209 } 210 211 auto origin = params.origin; 212 auto size = params.size; 213 auto cursor = params.cursor; 214 measureText(params, textRange); 215 auto alignmentOffset = rectAlignmentOffset(ivec2(params.size), halign, valign, area); 216 params.origin = origin + alignmentOffset; 217 params.cursor = cursor; 218 meshText!(true)(params, textRange); 219 } 220 221 ivec2 rectAlignmentOffset(ivec2 textSize, Alignment halign, Alignment valign, ivec2 area = ivec2(0,0)) 222 { 223 ivec2 offset = ivec2(alignOnAxis(textSize.x, halign, area.x), alignOnAxis(textSize.y, valign, area.y)); 224 return offset; 225 } 226 227 // returns true if rect is visible 228 bool clipTexturedRect(ref frect geometryRect, ref frect atlasRect, irect scissors) 229 { 230 frect intersection = rectIntersection(geometryRect, frect(scissors)); 231 if (intersection.empty) return false; 232 233 if (intersection == geometryRect) return true; 234 235 vec2 newEnd = intersection.endPosition; 236 vec2 newSize = intersection.size; 237 238 vec2 sizeRescaleMult = newSize / geometryRect.size; // [0; 1] 239 vec2 absStartOffset = intersection.position - geometryRect.position; 240 vec2 relativeStartOffset = absStartOffset / geometryRect.size; // [0; 1] 241 vec2 atlasPosOffset = relativeStartOffset * atlasRect.size; // old atlas size 242 243 atlasRect.size *= sizeRescaleMult; 244 atlasRect.position += atlasPosOffset; 245 246 geometryRect = intersection; 247 248 return true; 249 } 250 251 void meshTextf(P, Args...)(ref P params, string fmt, Args args) 252 { 253 import std.format : formattedWrite; 254 formattedWrite(TextMesherSink(¶ms), fmt, args); 255 }