Mercurial > games > semicongine
comparison semiconginev2/old/text.nim @ 1218:56781cc0fc7c compiletime-tests
did: renamge main package
| author | sam <sam@basx.dev> |
|---|---|
| date | Wed, 17 Jul 2024 21:01:37 +0700 |
| parents | semicongine/old/text.nim@a3eb305bcac2 |
| children |
comparison
equal
deleted
inserted
replaced
| 1217:f819a874058f | 1218:56781cc0fc7c |
|---|---|
| 1 import std/tables | |
| 2 import std/algorithm | |
| 3 import std/unicode | |
| 4 import std/strformat | |
| 5 | |
| 6 import ./core | |
| 7 import ./mesh | |
| 8 import ./material | |
| 9 import ./vulkan/shader | |
| 10 | |
| 11 const | |
| 12 NEWLINE = Rune('\n') | |
| 13 SPACE = Rune(' ') | |
| 14 | |
| 15 # font shader | |
| 16 MAX_TEXT_MATERIALS = 64 # need for every different font AND color | |
| 17 SHADER_ATTRIB_PREFIX = "semicon_text_" | |
| 18 POSITION_ATTRIB = SHADER_ATTRIB_PREFIX & "position" | |
| 19 UV_ATTRIB = SHADER_ATTRIB_PREFIX & "uv" | |
| 20 TEXT_MATERIAL_TYPE* = MaterialType( | |
| 21 name: "default-text-material-type", | |
| 22 vertexAttributes: {POSITION_ATTRIB: Vec3F32, UV_ATTRIB: Vec2F32}.toTable, | |
| 23 instanceAttributes: {TRANSFORM_ATTRIB: Mat4F32, MATERIALINDEX_ATTRIBUTE: UInt16}.toTable, | |
| 24 attributes: {"fontAtlas": TextureType, "color": Vec4F32}.toTable, | |
| 25 ) | |
| 26 TEXT_SHADER* = CreateShaderConfiguration( | |
| 27 name = "font shader", | |
| 28 inputs = [ | |
| 29 Attr[Mat4](TRANSFORM_ATTRIB, memoryPerformanceHint = PreferFastWrite, perInstance = true), | |
| 30 Attr[Vec3f](POSITION_ATTRIB, memoryPerformanceHint = PreferFastWrite), | |
| 31 Attr[Vec2f](UV_ATTRIB, memoryPerformanceHint = PreferFastWrite), | |
| 32 Attr[uint16](MATERIALINDEX_ATTRIBUTE, memoryPerformanceHint = PreferFastRead, perInstance = true), | |
| 33 ], | |
| 34 intermediates = [ | |
| 35 Attr[Vec2f]("uvFrag"), | |
| 36 Attr[uint16]("materialIndexOut", noInterpolation = true) | |
| 37 ], | |
| 38 outputs = [Attr[Vec4f]("color")], | |
| 39 uniforms = [Attr[Vec4f]("color", arrayCount = MAX_TEXT_MATERIALS), Attr[float32](ASPECT_RATIO_ATTRIBUTE)], | |
| 40 samplers = [Attr[Texture]("fontAtlas", arrayCount = MAX_TEXT_MATERIALS)], | |
| 41 vertexCode = &""" | |
| 42 gl_Position = vec4({POSITION_ATTRIB}.x, {POSITION_ATTRIB}.y * Uniforms.{ASPECT_RATIO_ATTRIBUTE}, {POSITION_ATTRIB}.z, 1.0) * {TRANSFORM_ATTRIB}; | |
| 43 uvFrag = {UV_ATTRIB}; | |
| 44 materialIndexOut = {MATERIALINDEX_ATTRIBUTE}; | |
| 45 """, | |
| 46 fragmentCode = &"""color = vec4(Uniforms.color[materialIndexOut].rgb, Uniforms.color[materialIndexOut].a * texture(fontAtlas[materialIndexOut], uvFrag).r);""" | |
| 47 ) | |
| 48 | |
| 49 var instanceCounter = 0 | |
| 50 | |
| 51 type | |
| 52 Text* = object | |
| 53 maxLen*: int | |
| 54 font*: Font | |
| 55 maxWidth: float32 = 0 | |
| 56 # properties: | |
| 57 text: seq[Rune] | |
| 58 horizontalAlignment: HorizontalAlignment = Center | |
| 59 verticalAlignment: VerticalAlignment = Center | |
| 60 # management/internal: | |
| 61 dirty: bool # is true if any of the attributes changed | |
| 62 processedText: seq[Rune] # used to store processed (word-wrapper) text to preserve original | |
| 63 lastRenderedText: seq[Rune] # stores the last rendered text, to prevent unnecessary updates | |
| 64 mesh*: Mesh | |
| 65 | |
| 66 func `$`*(text: Text): string = | |
| 67 "\"" & $text.text[0 ..< min(text.text.len, 16)] & "\"" | |
| 68 | |
| 69 proc Refresh*(text: var Text) = | |
| 70 if not text.dirty and text.processedText == text.lastRenderedText: | |
| 71 return | |
| 72 | |
| 73 # pre-calculate text-width | |
| 74 var width = 0'f32 | |
| 75 var lineWidths: seq[float32] | |
| 76 for i in 0 ..< text.processedText.len: | |
| 77 if text.processedText[i] == NEWLINE: | |
| 78 lineWidths.add width | |
| 79 width = 0'f32 | |
| 80 else: | |
| 81 if not (i == text.processedText.len - 1 and text.processedText[i].isWhiteSpace): | |
| 82 width += text.font.glyphs[text.processedText[i]].advance | |
| 83 if i < text.processedText.len - 1: | |
| 84 width += text.font.kerning[(text.processedText[i], text.processedText[i + 1])] | |
| 85 lineWidths.add width | |
| 86 var height = float32(lineWidths.len - 1) * text.font.lineAdvance + text.font.capHeight | |
| 87 if lineWidths[^1] == 0 and lineWidths.len > 1: | |
| 88 height -= 1 | |
| 89 | |
| 90 let anchorY = (case text.verticalAlignment | |
| 91 of Top: 0'f32 | |
| 92 of Center: height / 2 | |
| 93 of Bottom: height) - text.font.capHeight | |
| 94 | |
| 95 var | |
| 96 offsetX = 0'f32 | |
| 97 offsetY = 0'f32 | |
| 98 lineIndex = 0 | |
| 99 anchorX = case text.horizontalAlignment | |
| 100 of Left: 0'f32 | |
| 101 of Center: lineWidths[lineIndex] / 2 | |
| 102 of Right: lineWidths[lineIndex] | |
| 103 for i in 0 ..< text.maxLen: | |
| 104 let vertexOffset = i * 4 | |
| 105 if i < text.processedText.len: | |
| 106 if text.processedText[i] == Rune('\n'): | |
| 107 offsetX = 0 | |
| 108 offsetY += text.font.lineAdvance | |
| 109 text.mesh[POSITION_ATTRIB, vertexOffset + 0] = NewVec3f() | |
| 110 text.mesh[POSITION_ATTRIB, vertexOffset + 1] = NewVec3f() | |
| 111 text.mesh[POSITION_ATTRIB, vertexOffset + 2] = NewVec3f() | |
| 112 text.mesh[POSITION_ATTRIB, vertexOffset + 3] = NewVec3f() | |
| 113 inc lineIndex | |
| 114 anchorX = case text.horizontalAlignment | |
| 115 of Left: 0'f32 | |
| 116 of Center: lineWidths[lineIndex] / 2 | |
| 117 of Right: lineWidths[lineIndex] | |
| 118 else: | |
| 119 let | |
| 120 glyph = text.font.glyphs[text.processedText[i]] | |
| 121 left = offsetX + glyph.leftOffset | |
| 122 right = offsetX + glyph.leftOffset + glyph.dimension.x | |
| 123 top = offsetY + glyph.topOffset | |
| 124 bottom = offsetY + glyph.topOffset + glyph.dimension.y | |
| 125 | |
| 126 text.mesh[POSITION_ATTRIB, vertexOffset + 0] = NewVec3f(left - anchorX, bottom - anchorY) | |
| 127 text.mesh[POSITION_ATTRIB, vertexOffset + 1] = NewVec3f(left - anchorX, top - anchorY) | |
| 128 text.mesh[POSITION_ATTRIB, vertexOffset + 2] = NewVec3f(right - anchorX, top - anchorY) | |
| 129 text.mesh[POSITION_ATTRIB, vertexOffset + 3] = NewVec3f(right - anchorX, bottom - anchorY) | |
| 130 | |
| 131 text.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0] | |
| 132 text.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1] | |
| 133 text.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2] | |
| 134 text.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3] | |
| 135 | |
| 136 offsetX += glyph.advance | |
| 137 if i < text.processedText.len - 1: | |
| 138 offsetX += text.font.kerning[(text.processedText[i], text.processedText[i + 1])] | |
| 139 else: | |
| 140 text.mesh[POSITION_ATTRIB, vertexOffset + 0] = NewVec3f() | |
| 141 text.mesh[POSITION_ATTRIB, vertexOffset + 1] = NewVec3f() | |
| 142 text.mesh[POSITION_ATTRIB, vertexOffset + 2] = NewVec3f() | |
| 143 text.mesh[POSITION_ATTRIB, vertexOffset + 3] = NewVec3f() | |
| 144 text.lastRenderedText = text.processedText | |
| 145 text.dirty = false | |
| 146 | |
| 147 | |
| 148 func width(text: seq[Rune], font: Font): float32 = | |
| 149 var currentWidth = 0'f32 | |
| 150 var lineWidths: seq[float32] | |
| 151 for i in 0 ..< text.len: | |
| 152 if text[i] == NEWLINE: | |
| 153 lineWidths.add currentWidth | |
| 154 currentWidth = 0'f32 | |
| 155 else: | |
| 156 if not (i == text.len - 1 and text[i].isWhiteSpace): | |
| 157 currentWidth += font.glyphs[text[i]].advance | |
| 158 if i < text.len - 1: | |
| 159 currentWidth += font.kerning[(text[i], text[i + 1])] | |
| 160 lineWidths.add currentWidth | |
| 161 return lineWidths.max | |
| 162 | |
| 163 func wordWrapped(text: seq[Rune], font: Font, maxWidth: float32): seq[Rune] = | |
| 164 var remaining: seq[seq[Rune]] = @[@[]] | |
| 165 for c in text: | |
| 166 if c == SPACE: | |
| 167 remaining.add newSeq[Rune]() | |
| 168 else: | |
| 169 remaining[^1].add c | |
| 170 remaining.reverse() | |
| 171 | |
| 172 var currentLine: seq[Rune] | |
| 173 | |
| 174 while remaining.len > 0: | |
| 175 var currentWord = remaining.pop() | |
| 176 assert not (SPACE in currentWord) | |
| 177 | |
| 178 if currentWord.len == 0: | |
| 179 currentLine.add SPACE | |
| 180 else: | |
| 181 assert currentWord[^1] != SPACE | |
| 182 # if this is the first word of the line and it is too long we need to | |
| 183 # split by character | |
| 184 if currentLine.len == 0 and (SPACE & currentWord).width(font) > maxWidth: | |
| 185 var subWord = @[currentWord[0]] | |
| 186 for c in currentWord[1 .. ^1]: | |
| 187 if (subWord & c).width(font) > maxWidth: | |
| 188 break | |
| 189 subWord.add c | |
| 190 result.add subWord & NEWLINE | |
| 191 remaining.add currentWord[subWord.len .. ^1] # process rest of the word in next iteration | |
| 192 else: | |
| 193 if (currentLine & SPACE & currentWord).width(font) <= maxWidth: | |
| 194 if currentLine.len == 0: | |
| 195 currentLine = currentWord | |
| 196 else: | |
| 197 currentLine = currentLine & SPACE & currentWord | |
| 198 else: | |
| 199 result.add currentLine & NEWLINE | |
| 200 remaining.add currentWord | |
| 201 currentLine = @[] | |
| 202 if currentLine.len > 0 and currentLine != @[SPACE]: | |
| 203 result.add currentLine | |
| 204 | |
| 205 return result | |
| 206 | |
| 207 | |
| 208 func text*(text: Text): seq[Rune] = | |
| 209 text.text | |
| 210 | |
| 211 proc `text=`*(text: var Text, newText: seq[Rune]) = | |
| 212 text.text = newText[0 ..< min(newText.len, text.maxLen)] | |
| 213 | |
| 214 text.processedText = text.text | |
| 215 if text.maxWidth > 0: | |
| 216 text.processedText = text.processedText.wordWrapped(text.font, text.maxWidth / text.mesh.transform.Scaling.x) | |
| 217 | |
| 218 proc `text=`*(text: var Text, newText: string) = | |
| 219 `text=`(text, newText.toRunes) | |
| 220 | |
| 221 proc Color*(text: Text): Vec4f = | |
| 222 text.mesh.material["color", 0, Vec4f] | |
| 223 proc `Color=`*(text: var Text, value: Vec4f) = | |
| 224 if value != text.mesh.material["color", 0, Vec4f]: | |
| 225 text.mesh.material["color", 0] = value | |
| 226 | |
| 227 proc HorizontalAlignment*(text: Text): HorizontalAlignment = | |
| 228 text.horizontalAlignment | |
| 229 proc `horizontalAlignment=`*(text: var Text, value: HorizontalAlignment) = | |
| 230 if value != text.horizontalAlignment: | |
| 231 text.horizontalAlignment = value | |
| 232 text.dirty = true | |
| 233 | |
| 234 proc VerticalAlignment*(text: Text): VerticalAlignment = | |
| 235 text.verticalAlignment | |
| 236 proc `verticalAlignment=`*(text: var Text, value: VerticalAlignment) = | |
| 237 if value != text.verticalAlignment: | |
| 238 text.verticalAlignment = value | |
| 239 text.dirty = true | |
| 240 | |
| 241 proc InitText*(font: Font, text = "".toRunes, maxLen: int = text.len, color = NewVec4f(0.07, 0.07, 0.07, 1), verticalAlignment: VerticalAlignment = Center, horizontalAlignment: HorizontalAlignment = Center, maxWidth = 0'f32, transform = Unit4): Text = | |
| 242 var | |
| 243 positions = newSeq[Vec3f](int(maxLen * 4)) | |
| 244 indices: seq[array[3, uint16]] | |
| 245 uvs = newSeq[Vec2f](int(maxLen * 4)) | |
| 246 for i in 0 ..< maxLen: | |
| 247 let offset = i * 4 | |
| 248 indices.add [ | |
| 249 [uint16(offset + 0), uint16(offset + 1), uint16(offset + 2)], | |
| 250 [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)], | |
| 251 ] | |
| 252 | |
| 253 result = Text(maxLen: maxLen, font: font, dirty: true, horizontalAlignment: horizontalAlignment, verticalAlignment: verticalAlignment, maxWidth: maxWidth) | |
| 254 result.mesh = NewMesh(positions = positions, indices = indices, uvs = uvs, name = &"text-{instanceCounter}") | |
| 255 result.mesh[].RenameAttribute("position", POSITION_ATTRIB) | |
| 256 result.mesh[].RenameAttribute("uv", UV_ATTRIB) | |
| 257 result.mesh.material = TEXT_MATERIAL_TYPE.InitMaterialData( | |
| 258 name = font.name & " text", | |
| 259 attributes = {"fontAtlas": InitDataList(@[font.fontAtlas]), "color": InitDataList(@[color])}, | |
| 260 ) | |
| 261 result.mesh.transform = transform | |
| 262 `text=`(result, text) | |
| 263 inc instanceCounter | |
| 264 | |
| 265 result.Refresh() |
