changeset 879:7ecae083b670

add: correct version of text-alignment, and a few improvments
author Sam <sam@basx.dev>
date Sun, 28 Jan 2024 21:26:39 +0700
parents 8ab77af322cc
children 76380f64edac
files semicongine/algorithms.nim semicongine/text.nim tests/test_font.nim
diffstat 3 files changed, 147 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/algorithms.nim	Sun Jan 28 00:41:11 2024 +0700
+++ b/semicongine/algorithms.nim	Sun Jan 28 21:26:39 2024 +0700
@@ -1,5 +1,4 @@
 import std/algorithm
-import std/sequtils
 
 import ./core
 
@@ -18,19 +17,6 @@
 
 # FYI: also serves as "overlaps"
 func advanceIfOverlap(fix, newRect: Rect): (bool, int) =
-  let
-    p1 = [
-      (fix.x,             fix.y),
-      (fix.x + fix.w - 1, fix.y),
-      (fix.x,             fix.y + fix.h - 1),
-      (fix.x + fix.w - 1, fix.y + fix.h - 1)
-    ]
-    p2 = [
-      (newRect.x,                 newRect.y),
-      (newRect.x + newRect.w - 1, newRect.y),
-      (newRect.x,                 newRect.y + newRect.h - 1),
-      (newRect.x + newRect.w - 1, newRect.y + newRect.h - 1)
-    ]
   let overlapping = overlap(fix.x, fix.x + fix.w - 1, newRect.x, newRect.x + newRect.w - 1) and
                     overlap(fix.y, fix.y + fix.h - 1, newRect.y, newRect.y + newRect.h - 1)
   if overlapping: (true, fix.x + fix.w) # next free x coordinate to the right
@@ -72,7 +58,7 @@
 
   for area in areasBySize:
     var pos = find_insertion_position(assignedAreas, area, maxDim)
-    while not pos[0]: # this should actually never loop more than once, but weird things happen ¯\_(ツ)_/¯ 
+    while not pos[0]: # this should actually never loop more than once, but weird things happen ¯\_(ツ)_/¯
       maxDim = maxDim * 2
       assert maxDim <= MAX_ATLAS_SIZE, &"Atlas gets bigger than {MAX_ATLAS_SIZE}, cannot pack images"
       pos = find_insertion_position(assignedAreas, area, maxDim)
@@ -89,7 +75,6 @@
   for rect in assignedAreas:
     for y in 0 ..< rect.h:
       for x in 0 ..< rect.w:
-        let v = images[rect.i][x, y]
         assert result.atlas[rect.x + x, rect.y + y] == 0, "Atlas texture packing encountered an overlap error"
         result.atlas[rect.x + x, rect.y + y] = images[rect.i][x, y]
         result.coords[rect.i] = (x: rect.x, y: rect.y)
--- a/semicongine/text.nim	Sun Jan 28 00:41:11 2024 +0700
+++ b/semicongine/text.nim	Sun Jan 28 21:26:39 2024 +0700
@@ -12,23 +12,29 @@
 var instanceCounter = 0
 
 type
-  HorizontalAlignment = enum
+  HorizontalAlignment* = enum
     Left
     Center
     Right
-  VerticalAlignment = enum
+  VerticalAlignment* = enum
     Top
     Center
     Bottom
   Text* = object
     maxLen*: int
+    font*: Font
+    color*: Vec4f
     text: seq[Rune]
-    dirty: bool
-    horizontalAlignment*: HorizontalAlignment = Center
-    verticalAlignment*: VerticalAlignment = Center
-    font*: Font
-    mesh*: Mesh
-    color*: Vec4f
+    # attributes:
+    position: Vec2f
+    horizontalAlignment: HorizontalAlignment = Center
+    verticalAlignment: VerticalAlignment = Center
+    scale: float32
+    aspect_ratio: float32
+    # management:
+    dirty: bool                 # is true if any of the attributes changed
+    lastRenderedText: seq[Rune] # stores the last rendered text, to prevent unnecessary updates
+    mesh: Mesh
 
 const
   NEWLINE = Rune('\n')
@@ -53,106 +59,131 @@
     fragmentCode = &"""color = vec4(Uniforms.color.rgb, Uniforms.color.a * texture(fontAtlas, uvFrag).r);"""
   )
 
-proc updateMesh(textbox: var Text) =
+func `$`*(text: Text): string =
+  "\"" & $text.text[0 ..< min(text.text.len, 16)] & "\""
 
+proc refresh*(text: var Text) =
+  if not text.dirty and text.text == text.lastRenderedText:
+    return
+
+  echo "Refresh ", text
   # pre-calculate text-width
   var width = 0'f32
   var lineWidths: seq[float32]
-  for i in 0 ..< min(textbox.text.len, textbox.maxLen):
-    if textbox.text[i] == NEWLINE:
+  for i in 0 ..< min(text.text.len, text.maxLen):
+    if text.text[i] == NEWLINE:
       lineWidths.add width
       width = 0'f32
     else:
-      width += textbox.font.glyphs[textbox.text[i]].advance
-      if i < textbox.text.len - 1:
-        width += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
+      width += text.font.glyphs[text.text[i]].advance
+      if i < text.text.len - 1:
+        width += text.font.kerning[(text.text[i], text.text[i + 1])]
   lineWidths.add width
   let
-    height = float32(lineWidths.len) * textbox.font.lineAdvance
+    height = float32(lineWidths.len) * text.font.lineAdvance
 
-  let anchorY = (case textbox.verticalAlignment
+  let anchorY = (case text.verticalAlignment
     of Top: 0'f32
     of Center: height / 2
-    of Bottom: height) - textbox.font.lineAdvance
+    of Bottom: height) - text.font.lineAdvance
 
   var
     offsetX = 0'f32
     offsetY = 0'f32
     lineIndex = 0
-    anchorX = case textbox.horizontalAlignment
+    anchorX = case text.horizontalAlignment
       of Left: 0'f32
       of Center: lineWidths[lineIndex] / 2
-      of Right: lineWidths.max
-  for i in 0 ..< textbox.maxLen:
+      of Right: lineWidths[lineIndex]
+  for i in 0 ..< text.maxLen:
     let vertexOffset = i * 4
-    if i < textbox.text.len:
-      if textbox.text[i] == Rune('\n'):
+    if i < text.text.len:
+      if text.text[i] == Rune('\n'):
         offsetX = 0
-        offsetY += textbox.font.lineAdvance
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f()
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f()
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f()
+        offsetY += text.font.lineAdvance
+        text.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f()
+        text.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
+        text.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f()
+        text.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f()
         inc lineIndex
-        anchorX = case textbox.horizontalAlignment
+        anchorX = case text.horizontalAlignment
           of Left: 0'f32
           of Center: lineWidths[lineIndex] / 2
-          of Right: lineWidths.max
+          of Right: lineWidths[lineIndex]
       else:
         let
-          glyph = textbox.font.glyphs[textbox.text[i]]
+          glyph = text.font.glyphs[text.text[i]]
           left = offsetX + glyph.leftOffset
           right = offsetX + glyph.leftOffset + glyph.dimension.x
           top = offsetY + glyph.topOffset
           bottom = offsetY + glyph.topOffset + glyph.dimension.y
 
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f(left - anchorX, bottom - anchorY)
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f(left - anchorX, top - anchorY)
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f(right - anchorX, top - anchorY)
-        textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f(right - anchorX, bottom - anchorY)
+        text.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f(left - anchorX, bottom - anchorY)
+        text.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f(left - anchorX, top - anchorY)
+        text.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f(right - anchorX, top - anchorY)
+        text.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f(right - anchorX, bottom - anchorY)
 
-        textbox.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0]
-        textbox.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1]
-        textbox.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2]
-        textbox.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3]
+        text.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0]
+        text.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1]
+        text.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2]
+        text.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3]
 
         offsetX += glyph.advance
-        if i < textbox.text.len - 1:
-          offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
+        if i < text.text.len - 1:
+          offsetX += text.font.kerning[(text.text[i], text.text[i + 1])]
     else:
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f()
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f()
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f()
-
+      text.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f()
+      text.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
+      text.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f()
+      text.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f()
+  text.mesh.transform = translate(text.position.x, text.position.y, 0) * scale(text.scale, text.scale * text.aspect_ratio)
+  text.lastRenderedText = text.text
+  text.dirty = false
 
-func text*(textbox: Text): seq[Rune] =
-  textbox.text
+func text*(text: Text): seq[Rune] =
+  text.text
+proc `text=`*(text: var Text, newText: seq[Rune]) =
+  text.text = newText[0 ..< min(newText.len, text.maxLen)]
+proc `text=`*(text: var Text, newText: string) =
+  `text=`(text, newText.toRunes)
 
-proc `text=`*(textbox: var Text, text: seq[Rune]) =
-  let newText = text[0 ..< min(text.len, textbox.maxLen)]
-  if textbox.text != newText:
-    textbox.text = newText
-    textbox.updateMesh()
+proc position*(text: Text): Vec2f =
+  text.position
+proc `position=`*(text: var Text, value: Vec2f) =
+  if value != text.position:
+    text.position = value
+    text.dirty = true
+
+proc horizontalAlignment*(text: Text): HorizontalAlignment =
+  text.horizontalAlignment
 
-proc `text=`*(textbox: var Text, text: string) =
-  `text=`(textbox, text.toRunes)
+proc `horizontalAlignment=`*(text: var Text, value: HorizontalAlignment) =
+  if value != text.horizontalAlignment:
+    text.horizontalAlignment = value
+    text.dirty = true
+
+proc verticalAlignment*(text: Text): VerticalAlignment =
+  text.verticalAlignment
+proc `verticalAlignment=`*(text: var Text, value: VerticalAlignment) =
+  if value != text.verticalAlignment:
+    text.verticalAlignment = value
+    text.dirty = true
 
-proc horizontalAlignment*(textbox: Text): HorizontalAlignment =
-  textbox.horizontalAlignment
-proc verticalAlignment*(textbox: Text): VerticalAlignment =
-  textbox.verticalAlignment
-proc `horizontalAlignment=`*(textbox: var Text, value: HorizontalAlignment) =
-  if value != textbox.horizontalAlignment:
-    textbox.horizontalAlignment = value
-    textbox.updateMesh()
-proc `verticalAlignment=`*(textbox: var Text, value: VerticalAlignment) =
-  if value != textbox.verticalAlignment :
-    textbox.verticalAlignment = value
-    textbox.updateMesh()
+proc scale*(text: Text): float32 =
+  text.scale
+proc `scale=`*(text: var Text, value: float32) =
+  if value != text.scale:
+    text.scale = value
+    text.dirty = true
 
+proc aspect_ratio*(text: Text): float32 =
+  text.aspect_ratio
+proc `aspect_ratio=`*(text: var Text, value: float32) =
+  if value != text.aspect_ratio:
+    text.aspect_ratio = value
+    text.dirty = true
 
-proc initText*(maxLen: int, font: Font, text = "".toRunes, color = newVec4f(0, 0, 0, 1)): Text =
+proc initText*(font: Font, text = "".toRunes, maxLen: int = text.len, color = newVec4f(0.07, 0.07, 0.07, 1), scale = 1'f32, position = newVec2f(), verticalAlignment = VerticalAlignment.Center, horizontalAlignment = HorizontalAlignment.Center): Text =
   var
     positions = newSeq[Vec3f](int(maxLen * 4))
     indices: seq[array[3, uint16]]
@@ -164,19 +195,18 @@
       [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)],
     ]
 
-  result = Text(maxLen: maxLen, text: text, font: font, dirty: true)
-  result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs, name = &"textbox-{instanceCounter}")
+  result = Text(maxLen: maxLen, text: text, font: font, dirty: true, scale: scale, position: position, aspect_ratio: 1, horizontalAlignment: horizontalAlignment, verticalAlignment: verticalAlignment)
+  result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs, name = &"text-{instanceCounter}")
   inc instanceCounter
   result.mesh[].renameAttribute("position", POSITION_ATTRIB)
   result.mesh[].renameAttribute("uv", UV_ATTRIB)
   result.mesh.material = initMaterialData(
     theType = TEXT_MATERIAL_TYPE,
     name = font.name & " text",
-    attributes = {"fontAtlas": initDataList(@[font.fontAtlas]),
-        "color": initDataList(@[color])},
+    attributes = {"fontAtlas": initDataList(@[font.fontAtlas]), "color": initDataList(@[color])},
   )
 
-  result.updateMesh()
+  result.refresh()
 
-proc initText*(maxLen: int, font: Font, text = "", color = newVec4f(0, 0, 0, 1)): Text =
-  initText(maxLen = maxLen, font = font, text = text.toRunes, color = color)
+proc initText*(font: Font, text = "", maxLen: int = text.len, color = newVec4f(0.07, 0.07, 0.07, 1), scale = 1'f32, position = newVec2f(), verticalAlignment = VerticalAlignment.Center, horizontalAlignment = HorizontalAlignment.Center): Text =
+  initText(font = font, text = text.toRunes, maxLen = maxLen, color = color, scale = scale, position = position, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment)
--- a/tests/test_font.nim	Sun Jan 28 00:41:11 2024 +0700
+++ b/tests/test_font.nim	Sun Jan 28 21:26:39 2024 +0700
@@ -2,6 +2,7 @@
 
 import semicongine
 
+
 proc main() =
   # setup engine
   var engine = initEngine("Test fonts")
@@ -10,33 +11,60 @@
   # build scene
   var scene = Scene(name: "main")
   # var font = loadFont("DejaVuSans.ttf", lineHeightPixels=90'f32, charset="abcdefghijklmnopqrstuvwxyz ".toRunes)
-  var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 250'f32)
-  var textbox = initText(32, font, "_", color = newVec4f(1, 0, 0, 1))
-  let fontscale = 0.001
-  scene.add textbox
-  textbox.mesh.transform = scale(fontscale, fontscale)
+  var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 210'f32)
+  var main_text = font.initText("", 32, color = newVec4f(1, 0.15, 0.15, 1), scale = 0.001)
+  var help_text = font.initText("""Controls
+
+Horizontal alignment:
+  F1: Left
+  F2: Center
+  F3: Right
+Vertical alignment:
+  F4: Top
+  F5: Center
+  F6: Bottom""", scale = 0.0001, position = newVec2f(0, 0), horizontalAlignment = Left, verticalAlignment = Top)
+  scene.add main_text
+  scene.add help_text
   engine.loadScene(scene)
 
-  let cursor = Rune('_')
   while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
     if engine.windowWasResized():
       var winSize = engine.getWindow().size
-      textbox.mesh.transform = scale(fontscale * (winSize[1] / winSize[0]), fontscale)
-    if textbox.text.len < textbox.maxLen - 1:
+      main_text.aspect_ratio = winSize[0] / winSize[1]
+      help_text.aspect_ratio = winSize[0] / winSize[1]
+      help_text.position = newVec2f(-0.99, -0.99)
+
+    # add character
+    if main_text.text.len < main_text.maxLen - 1:
       for c in [Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I,
           Key.J, Key.K, Key.L, Key.M, Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S,
           Key.T, Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z]:
         if engine.keyWasPressed(c):
           if engine.keyIsDown(ShiftL) or engine.keyIsDown(ShiftR):
-            textbox.text = textbox.text[0 ..< ^1] & ($c).toRunes & cursor
+            main_text.text = main_text.text & ($c).toRunes
           else:
-            textbox.text = textbox.text[0 ..< ^1] & ($c).toRunes[0].toLower() & cursor
+            main_text.text = main_text.text & ($c).toRunes[0].toLower()
       if engine.keyWasPressed(Enter):
-        textbox.text = textbox.text[0 ..< ^1] & Rune('\n') & cursor
+        main_text.text = main_text.text & Rune('\n')
       if engine.keyWasPressed(Space):
-        textbox.text = textbox.text[0 ..< ^1] & Rune(' ') & cursor
-    if engine.keyWasPressed(Backspace) and textbox.text.len > 1:
-      textbox.text = textbox.text[0 ..< ^2] & cursor
+        main_text.text = main_text.text & Rune(' ')
+
+    # remove character
+    if engine.keyWasPressed(Backspace) and main_text.text.len > 0:
+      main_text.text = main_text.text[0 ..< ^1]
+
+    # alignemtn with F-keys
+    if engine.keyWasPressed(F1): main_text.horizontalAlignment = Left
+    elif engine.keyWasPressed(F2): main_text.horizontalAlignment = Center
+    elif engine.keyWasPressed(F3): main_text.horizontalAlignment = Right
+    elif engine.keyWasPressed(F4): main_text.verticalAlignment = Top
+    elif engine.keyWasPressed(F5): main_text.verticalAlignment = Center
+    elif engine.keyWasPressed(F6): main_text.verticalAlignment = Bottom
+
+    main_text.text = main_text.text & Rune('_')
+    main_text.refresh()
+    main_text.text = main_text.text[0 ..< ^1]
+    help_text.refresh()
     engine.renderScene(scene)
   engine.destroy()