changeset 1475:e4eed5f9ac33 default tip

did: cleanup text/font code
author sam <sam@basx.dev>
date Mon, 07 Apr 2025 23:58:41 +0700
parents bb7bbe2fee78
children
files semicongine/core/types.nim semicongine/fonts.nim semicongine/text.nim semicongine/text/textbox.nim
diffstat 4 files changed, 89 insertions(+), 298 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/types.nim	Sun Apr 06 21:56:10 2025 +0700
+++ b/semicongine/core/types.nim	Mon Apr 07 23:58:41 2025 +0700
@@ -337,73 +337,6 @@
   ImageArray*[T: PixelType] = ImageObject[T, true]
 
   # === fonts ===
-  GlyphQuad*[MaxGlyphs: static int] = object
-    # vertex offsets to glyph center: [left, bottom, right, top]
-    pos*: array[MaxGlyphs, Vec4f]
-    uv*: array[MaxGlyphs, Vec4f] # [left, bottom, right, top]
-
-  TextRendering* = object
-    aspectRatio*: float32
-
-  GlyphDescriptorSet*[MaxGlyphs: static int] = object
-    fontAtlas*: Image[Gray]
-    glyphquads*: GPUValue[GlyphQuad[MaxGlyphs], StorageBuffer]
-
-  GlyphShader*[MaxGlyphs: static int] = object
-    position {.InstanceAttribute.}: Vec3f
-    color {.InstanceAttribute.}: Vec4f
-    scale {.InstanceAttribute.}: float32
-    glyphIndex {.InstanceAttribute.}: uint16
-    textRendering {.PushConstant.}: TextRendering
-
-    fragmentUv {.Pass.}: Vec2f
-    fragmentColor {.PassFlat.}: Vec4f
-    outColor {.ShaderOutput.}: Vec4f
-    glyphData {.DescriptorSet: 3.}: GlyphDescriptorSet[MaxGlyphs]
-    vertexCode* =
-      """
-const int[6] indices = int[](0, 1, 2, 2, 3, 0);
-const int[4] i_x = int[](0, 0, 2, 2);
-const int[4] i_y = int[](1, 3, 3, 1);
-const float epsilon = 0.0000001;
-
-void main() {
-  int vertexI = indices[gl_VertexIndex];
-  vec3 vertexPos = vec3(
-    glyphquads.pos[glyphIndex][i_x[vertexI]] * scale / textRendering.aspectRatio,
-    glyphquads.pos[glyphIndex][i_y[vertexI]] * scale,
-    0
-  );
-  // the epsilon-offset is necessary, as otherwise characters with the same Z might overlap, despite transparency
-  gl_Position = vec4(vertexPos + position, 1.0);
-  gl_Position.z -= gl_InstanceIndex * epsilon;
-  gl_Position.z = fract(abs(gl_Position.z));
-  vec2 uv = vec2(glyphquads.uv[glyphIndex][i_x[vertexI]], glyphquads.uv[glyphIndex][i_y[vertexI]]);
-  fragmentUv = uv;
-  fragmentColor = color;
-}  """
-    fragmentCode* =
-      """void main() {
-    float a = texture(fontAtlas, fragmentUv).r;
-    outColor = vec4(fragmentColor.rgb, fragmentColor.a * a);
-}"""
-
-  FontObj*[MaxGlyphs: static int] = object
-    advance*: Table[Rune, float32]
-    kerning*: Table[(Rune, Rune), float32]
-    leftBearing*: Table[Rune, float32]
-    lineAdvance*: float32
-    lineHeight*: float32 # like lineAdvance - lineGap
-    ascent*: float32 # from baseline to highest glyph
-    descent*: float32 # from baseline to lowest glyph
-    xHeight*: float32 # from baseline to height of lowercase x
-    descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]]
-    descriptorGlyphIndex*: Table[Rune, uint16]
-    descriptorGlyphIndexRev*: Table[uint16, Rune] # only used for debugging atm
-    fallbackCharacter*: Rune
-
-  Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs]
-
   TextHandle* = object
     index*: uint32
     generation*: uint32
@@ -423,17 +356,6 @@
     color*: Vec4f = vec4(1, 1, 1, 1)
     capacity*: int
 
-  TextBuffer*[MaxGlyphs: static int] = object
-    cursor*: int
-    generation*: uint32
-    font*: Font[MaxGlyphs]
-    baseScale*: float32
-    position*: GPUArray[Vec3f, VertexBufferMapped]
-    color*: GPUArray[Vec4f, VertexBufferMapped]
-    scale*: GPUArray[float32, VertexBufferMapped]
-    glyphIndex*: GPUArray[uint16, VertexBufferMapped]
-    texts*: seq[Text]
-
   # === background loader thread ===
   LoaderThreadArgs*[T] = (
     ptr Channel[(string, string)],
@@ -521,10 +443,3 @@
 proc `=copy`[S, T](dest: var ImageObject[S, T], source: ImageObject[S, T]) {.error.}
 proc `=copy`(dest: var Input, source: Input) {.error.}
 proc `=copy`(dest: var EngineObj, source: EngineObj) {.error.}
-proc `=copy`[MaxGlyphs: static int](
-  dest: var FontObj[MaxGlyphs], source: FontObj[MaxGlyphs]
-) {.error.}
-
-proc `=copy`[MaxGlyphs: static int](
-  dest: var TextBuffer[MaxGlyphs], source: TextBuffer[MaxGlyphs]
-) {.error.}
--- a/semicongine/fonts.nim	Sun Apr 06 21:56:10 2025 +0700
+++ b/semicongine/fonts.nim	Mon Apr 07 23:58:41 2025 +0700
@@ -22,6 +22,36 @@
 
 const ASCII_CHARSET = PrintableChars.toSeq.toRunes
 
+type
+  GlyphQuad*[MaxGlyphs: static int] = object
+    # vertex offsets to glyph center: [left, bottom, right, top]
+    pos*: array[MaxGlyphs, Vec4f]
+    uv*: array[MaxGlyphs, Vec4f] # [left, bottom, right, top]
+
+  GlyphDescriptorSet*[MaxGlyphs: static int] = object
+    fontAtlas*: Image[Gray]
+    glyphquads*: GPUValue[GlyphQuad[MaxGlyphs], StorageBuffer]
+
+  FontObj*[MaxGlyphs: static int] = object
+    advance*: Table[Rune, float32]
+    kerning*: Table[(Rune, Rune), float32]
+    leftBearing*: Table[Rune, float32]
+    lineAdvance*: float32
+    lineHeight*: float32 # like lineAdvance - lineGap
+    ascent*: float32 # from baseline to highest glyph
+    descent*: float32 # from baseline to lowest glyph
+    xHeight*: float32 # from baseline to height of lowercase x
+    descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]]
+    descriptorGlyphIndex*: Table[Rune, uint16]
+    descriptorGlyphIndexRev*: Table[uint16, Rune] # only used for debugging atm
+    fallbackCharacter*: Rune
+
+  Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs]
+
+proc `=copy`[MaxGlyphs: static int](
+  dest: var FontObj[MaxGlyphs], source: FontObj[MaxGlyphs]
+) {.error.}
+
 type stbtt_fontinfo {.importc, incompleteStruct.} = object
 
 proc stbtt_InitFont(
--- a/semicongine/text.nim	Sun Apr 06 21:56:10 2025 +0700
+++ b/semicongine/text.nim	Mon Apr 07 23:58:41 2025 +0700
@@ -11,6 +11,65 @@
 import ./images
 import ./rendering/renderer
 import ./rendering/memory
+import ./fonts
+
+type
+  TextRendering* = object
+    aspectRatio*: float32
+
+  GlyphShader*[MaxGlyphs: static int] = object
+    position {.InstanceAttribute.}: Vec3f
+    color {.InstanceAttribute.}: Vec4f
+    scale {.InstanceAttribute.}: float32
+    glyphIndex {.InstanceAttribute.}: uint16
+    textRendering {.PushConstant.}: TextRendering
+
+    fragmentUv {.Pass.}: Vec2f
+    fragmentColor {.PassFlat.}: Vec4f
+    outColor {.ShaderOutput.}: Vec4f
+    glyphData {.DescriptorSet: 3.}: GlyphDescriptorSet[MaxGlyphs]
+    vertexCode* =
+      """
+  const int[6] indices = int[](0, 1, 2, 2, 3, 0);
+  const int[4] i_x = int[](0, 0, 2, 2);
+  const int[4] i_y = int[](1, 3, 3, 1);
+  const float epsilon = 0.0000001;
+
+  void main() {
+    int vertexI = indices[gl_VertexIndex];
+    vec3 vertexPos = vec3(
+      glyphquads.pos[glyphIndex][i_x[vertexI]] * scale / textRendering.aspectRatio,
+      glyphquads.pos[glyphIndex][i_y[vertexI]] * scale,
+      0
+    );
+    // the epsilon-offset is necessary, as otherwise characters with the same Z might overlap, despite transparency
+    gl_Position = vec4(vertexPos + position, 1.0);
+    gl_Position.z -= gl_InstanceIndex * epsilon;
+    gl_Position.z = fract(abs(gl_Position.z));
+    vec2 uv = vec2(glyphquads.uv[glyphIndex][i_x[vertexI]], glyphquads.uv[glyphIndex][i_y[vertexI]]);
+    fragmentUv = uv;
+    fragmentColor = color;
+  }  """
+    fragmentCode* =
+      """void main() {
+      float a = texture(fontAtlas, fragmentUv).r;
+      outColor = vec4(fragmentColor.rgb, fragmentColor.a * a);
+  }"""
+
+  TextBuffer*[MaxGlyphs: static int] = object
+    cursor*: int
+    generation*: uint32
+    font*: Font[MaxGlyphs]
+    baseScale*: float32
+    position*: GPUArray[Vec3f, VertexBufferMapped]
+    color*: GPUArray[Vec4f, VertexBufferMapped]
+    scale*: GPUArray[float32, VertexBufferMapped]
+    glyphIndex*: GPUArray[uint16, VertexBufferMapped]
+    texts*: seq[Text]
+
+proc `=copy`[MaxGlyphs: static int](
+  dest: var TextBuffer[MaxGlyphs], source: TextBuffer[MaxGlyphs]
+) {.error.}
 
 proc initTextBuffer*[MaxGlyphs: static int](
     font: Font[MaxGlyphs],
--- a/semicongine/text/textbox.nim	Sun Apr 06 21:56:10 2025 +0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,213 +0,0 @@
-type Textbox* = object
-  font*: Font
-  maxLen*: int # maximum amount of characters that will be rendered
-  maxWidth: float32 = 0 # if set, will cause automatic word breaks at maxWidth
-  baseScale: float32
-  text: seq[Rune]
-  horizontalAlignment: HorizontalAlignment = Center
-  verticalAlignment: VerticalAlignment = Center
-  # management/internal:
-  dirtyGeometry: bool # is true if any of the attributes changed
-  dirtyShaderdata: bool # is true if any of the attributes changed
-  visibleText: seq[Rune]
-    # used to store processed (word-wrapper) text to preserve original
-  lastRenderedText: seq[Rune]
-    # stores the last rendered text, to prevent unnecessary updates
-
-  # rendering data
-  position: GPUArray[Vec3f, VertexBuffer]
-  uv: GPUArray[Vec2f, VertexBuffer]
-  indices: GPUArray[uint16, IndexBuffer]
-
-proc `=copy`(dest: var Textbox, source: Textbox) {.error.}
-
-func `$`*(textbox: Textbox): string =
-  "\"" & $textbox.text[0 ..< min(textbox.text.len, 16)] & "\""
-
-proc refreshGeometry(textbox: var Textbox) =
-  # pre-calculate text-width
-  var width = 0'f32
-  var lineWidths: seq[float32]
-  for i in 0 ..< textbox.visibleText.len:
-    if textbox.visibleText[i] == NEWLINE:
-      lineWidths.add width
-      width = 0'f32
-    else:
-      if not (i == textbox.visibleText.len - 1 and textbox.visibleText[i].isWhiteSpace):
-        width += textbox.font.glyphdata[textbox.visibleText[i]].advance
-      if i < textbox.visibleText.len - 1:
-        width +=
-          textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])]
-  lineWidths.add width
-  var height =
-    float32(lineWidths.len - 1) * textbox.font.lineAdvance + textbox.font.capHeight
-  if lineWidths[^1] == 0 and lineWidths.len > 1:
-    height -= 1
-
-  let anchorY =
-    (
-      case textbox.verticalAlignment
-      of Top: 0'f32
-      of Center: -height / 2
-      of Bottom: -height
-    ) + textbox.font.capHeight
-
-  var
-    offsetX = 0'f32
-    offsetY = 0'f32
-    lineIndex = 0
-    anchorX =
-      case textbox.horizontalAlignment
-      of Left:
-        0'f32
-      of Center:
-        lineWidths[lineIndex] / 2
-      of Right:
-        lineWidths[lineIndex]
-  for i in 0 ..< textbox.maxLen:
-    let vertexOffset = i * 4
-    if i < textbox.visibleText.len:
-      if textbox.visibleText[i] == Rune('\n'):
-        offsetX = 0
-        offsetY -= textbox.font.lineAdvance
-        textbox.position.data[vertexOffset + 0] = vec3(0, 0, 0)
-        textbox.position.data[vertexOffset + 1] = vec3(0, 0, 0)
-        textbox.position.data[vertexOffset + 2] = vec3(0, 0, 0)
-        textbox.position.data[vertexOffset + 3] = vec3(0, 0, 0)
-        inc lineIndex
-        anchorX =
-          case textbox.horizontalAlignment
-          of Left:
-            0'f32
-          of Center:
-            lineWidths[lineIndex] / 2
-          of Right:
-            lineWidths[lineIndex]
-      else:
-        let
-          glyph = textbox.font.glyphdata[textbox.visibleText[i]]
-          left = offsetX + glyph.offsetX
-          right = offsetX + glyph.offsetX + glyph.dimension.x
-          top = offsetY - glyph.offsetY
-          bottom = offsetY - glyph.offsetY - glyph.dimension.y
-
-        textbox.position.data[vertexOffset + 0] =
-          vec3(left - anchorX, bottom - anchorY, 0)
-        textbox.position.data[vertexOffset + 1] = vec3(left - anchorX, top - anchorY, 0)
-        textbox.position.data[vertexOffset + 2] =
-          vec3(right - anchorX, top - anchorY, 0)
-        textbox.position.data[vertexOffset + 3] =
-          vec3(right - anchorX, bottom - anchorY, 0)
-
-        textbox.uv.data[vertexOffset + 0] = glyph.uvs[0]
-        textbox.uv.data[vertexOffset + 1] = glyph.uvs[1]
-        textbox.uv.data[vertexOffset + 2] = glyph.uvs[2]
-        textbox.uv.data[vertexOffset + 3] = glyph.uvs[3]
-
-        offsetX += glyph.advance
-        if i < textbox.visibleText.len - 1:
-          offsetX +=
-            textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])]
-  updateGPUBuffer(textbox.position, count = textbox.visibleText.len.uint64 * 4)
-  updateGPUBuffer(textbox.uv, count = textbox.visibleText.len.uint64 * 4)
-  textbox.lastRenderedText = textbox.visibleText
-
-func text*(textbox: Textbox): seq[Rune] =
-  textbox.text
-
-proc `text=`*(textbox: var Textbox, newText: seq[Rune]) =
-  if newText[0 ..< min(newText.len, textbox.maxLen)] == textbox.text:
-    return
-
-  textbox.text = newText[0 ..< min(newText.len, textbox.maxLen)]
-
-  textbox.visibleText = textbox.text
-  if textbox.maxWidth > 0:
-    textbox.visibleText = WordWrapped(
-      textbox.visibleText, textbox.font[], textbox.maxWidth / textbox.baseScale
-    )
-
-proc `text=`*(textbox: var Textbox, newText: string) =
-  `text=`(textbox, newText.toRunes)
-
-proc horizontalAlignment*(textbox: Textbox): HorizontalAlignment =
-  textbox.horizontalAlignment
-
-proc `horizontalAlignment=`*(textbox: var Textbox, value: HorizontalAlignment) =
-  if value != textbox.horizontalAlignment:
-    textbox.horizontalAlignment = value
-    textbox.dirtyGeometry = true
-
-proc verticalAlignment*(textbox: Textbox): VerticalAlignment =
-  textbox.verticalAlignment
-
-proc `verticalAlignment=`*(textbox: var Textbox, value: VerticalAlignment) =
-  if value != textbox.verticalAlignment:
-    textbox.verticalAlignment = value
-    textbox.dirtyGeometry = true
-
-proc refresh*(textbox: var Textbox) =
-  if textbox.dirtyGeometry or textbox.visibleText != textbox.lastRenderedText:
-    textbox.refreshGeometry()
-    textbox.dirtyGeometry = false
-
-proc render*(
-    commandbuffer: VkCommandBuffer,
-    pipeline: Pipeline,
-    textbox: Textbox,
-    position: Vec3f,
-    color: Vec4f,
-    scale: Vec2f = vec2(1, 1),
-) =
-  renderWithPushConstant(
-    commandbuffer = commandbuffer,
-    pipeline = pipeline,
-    mesh = textbox,
-    pushConstant =
-      TextboxData(position: position, scale: textbox.baseScale * scale, color: color),
-    fixedVertexCount = textbox.visibleText.len * 6,
-  )
-
-proc initTextbox*[T: string | seq[Rune]](
-    renderdata: var RenderData,
-    descriptorSetLayout: VkDescriptorSetLayout,
-    font: Font,
-    baseScale: float32,
-    text: T = default(T),
-    maxLen: int = text.len,
-    verticalAlignment: VerticalAlignment = Center,
-    horizontalAlignment: HorizontalAlignment = Center,
-    maxWidth = 0'f32,
-): Textbox =
-  result = Textbox(
-    maxLen: maxLen,
-    font: font,
-    dirtyGeometry: true,
-    dirtyShaderdata: true,
-    horizontalAlignment: horizontalAlignment,
-    verticalAlignment: verticalAlignment,
-    maxWidth: maxWidth,
-    baseScale: baseScale,
-    position: asGPUArray(newSeq[Vec3f](int(maxLen * 4)), VertexBuffer),
-    uv: asGPUArray(newSeq[Vec2f](int(maxLen * 4)), VertexBuffer),
-    indices: asGPUArray(newSeq[uint16](int(maxLen * 6)), IndexBuffer),
-  )
-
-  for i in 0 ..< maxLen:
-    let vertexIndex = i.uint16 * 4'u16
-    result.indices.data[i * 6 + 0] = vertexIndex + 0
-    result.indices.data[i * 6 + 1] = vertexIndex + 1
-    result.indices.data[i * 6 + 2] = vertexIndex + 2
-    result.indices.data[i * 6 + 3] = vertexIndex + 2
-    result.indices.data[i * 6 + 4] = vertexIndex + 3
-    result.indices.data[i * 6 + 5] = vertexIndex + 0
-
-  when T is string:
-    `text=`(result, text.toRunes())
-  else:
-    `text=`(result, text)
-
-  assignBuffers(renderdata, result, uploadData = false)
-
-  result.refresh()
-  updateAllGPUBuffers(result, flush = true)