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(&params), fmt, args);
255 }