view semiconginev2/old/text.nim @ 1228:4e465583ea32

did: rename texture to image
author sam <sam@basx.dev>
date Thu, 18 Jul 2024 16:33:24 +0700
parents 56781cc0fc7c
children
line wrap: on
line source

import std/tables
import std/algorithm
import std/unicode
import std/strformat

import ./core
import ./mesh
import ./material
import ./vulkan/shader

const
  NEWLINE = Rune('\n')
  SPACE = Rune(' ')

  # font shader
  MAX_TEXT_MATERIALS = 64 # need for every different font AND color
  SHADER_ATTRIB_PREFIX = "semicon_text_"
  POSITION_ATTRIB = SHADER_ATTRIB_PREFIX & "position"
  UV_ATTRIB = SHADER_ATTRIB_PREFIX & "uv"
  TEXT_MATERIAL_TYPE* = MaterialType(
    name: "default-text-material-type",
    vertexAttributes: {POSITION_ATTRIB: Vec3F32, UV_ATTRIB: Vec2F32}.toTable,
    instanceAttributes: {TRANSFORM_ATTRIB: Mat4F32, MATERIALINDEX_ATTRIBUTE: UInt16}.toTable,
    attributes: {"fontAtlas": TextureType, "color": Vec4F32}.toTable,
  )
  TEXT_SHADER* = CreateShaderConfiguration(
    name = "font shader",
    inputs = [
      Attr[Mat4](TRANSFORM_ATTRIB, memoryPerformanceHint = PreferFastWrite, perInstance = true),
      Attr[Vec3f](POSITION_ATTRIB, memoryPerformanceHint = PreferFastWrite),
      Attr[Vec2f](UV_ATTRIB, memoryPerformanceHint = PreferFastWrite),
      Attr[uint16](MATERIALINDEX_ATTRIBUTE, memoryPerformanceHint = PreferFastRead, perInstance = true),
    ],
    intermediates = [
      Attr[Vec2f]("uvFrag"),
      Attr[uint16]("materialIndexOut", noInterpolation = true)
    ],
    outputs = [Attr[Vec4f]("color")],
    uniforms = [Attr[Vec4f]("color", arrayCount = MAX_TEXT_MATERIALS), Attr[float32](ASPECT_RATIO_ATTRIBUTE)],
    samplers = [Attr[Texture]("fontAtlas", arrayCount = MAX_TEXT_MATERIALS)],
    vertexCode = &"""
  gl_Position = vec4({POSITION_ATTRIB}.x, {POSITION_ATTRIB}.y * Uniforms.{ASPECT_RATIO_ATTRIBUTE}, {POSITION_ATTRIB}.z, 1.0) * {TRANSFORM_ATTRIB};
  uvFrag = {UV_ATTRIB};
  materialIndexOut = {MATERIALINDEX_ATTRIBUTE};
  """,
    fragmentCode = &"""color = vec4(Uniforms.color[materialIndexOut].rgb, Uniforms.color[materialIndexOut].a * texture(fontAtlas[materialIndexOut], uvFrag).r);"""
  )

var instanceCounter = 0

type
  Text* = object
    maxLen*: int
    font*: Font
    maxWidth: float32 = 0
    # properties:
    text: seq[Rune]
    horizontalAlignment: HorizontalAlignment = Center
    verticalAlignment: VerticalAlignment = Center
    # management/internal:
    dirty: bool                 # is true if any of the attributes changed
    processedText: seq[Rune]    # used to store processed (word-wrapper) text to preserve original
    lastRenderedText: seq[Rune] # stores the last rendered text, to prevent unnecessary updates
    mesh*: Mesh

func `$`*(text: Text): string =
  "\"" & $text.text[0 ..< min(text.text.len, 16)] & "\""

proc Refresh*(text: var Text) =
  if not text.dirty and text.processedText == text.lastRenderedText:
    return

  # pre-calculate text-width
  var width = 0'f32
  var lineWidths: seq[float32]
  for i in 0 ..< text.processedText.len:
    if text.processedText[i] == NEWLINE:
      lineWidths.add width
      width = 0'f32
    else:
      if not (i == text.processedText.len - 1 and text.processedText[i].isWhiteSpace):
        width += text.font.glyphs[text.processedText[i]].advance
      if i < text.processedText.len - 1:
        width += text.font.kerning[(text.processedText[i], text.processedText[i + 1])]
  lineWidths.add width
  var height = float32(lineWidths.len - 1) * text.font.lineAdvance + text.font.capHeight
  if lineWidths[^1] == 0 and lineWidths.len > 1:
    height -= 1

  let anchorY = (case text.verticalAlignment
    of Top: 0'f32
    of Center: height / 2
    of Bottom: height) - text.font.capHeight

  var
    offsetX = 0'f32
    offsetY = 0'f32
    lineIndex = 0
    anchorX = case text.horizontalAlignment
      of Left: 0'f32
      of Center: lineWidths[lineIndex] / 2
      of Right: lineWidths[lineIndex]
  for i in 0 ..< text.maxLen:
    let vertexOffset = i * 4
    if i < text.processedText.len:
      if text.processedText[i] == Rune('\n'):
        offsetX = 0
        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 text.horizontalAlignment
          of Left: 0'f32
          of Center: lineWidths[lineIndex] / 2
          of Right: lineWidths[lineIndex]
      else:
        let
          glyph = text.font.glyphs[text.processedText[i]]
          left = offsetX + glyph.leftOffset
          right = offsetX + glyph.leftOffset + glyph.dimension.x
          top = offsetY + glyph.topOffset
          bottom = offsetY + glyph.topOffset + glyph.dimension.y

        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)

        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 < text.processedText.len - 1:
          offsetX += text.font.kerning[(text.processedText[i], text.processedText[i + 1])]
    else:
      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.lastRenderedText = text.processedText
  text.dirty = false


func width(text: seq[Rune], font: Font): float32 =
  var currentWidth = 0'f32
  var lineWidths: seq[float32]
  for i in 0 ..< text.len:
    if text[i] == NEWLINE:
      lineWidths.add currentWidth
      currentWidth = 0'f32
    else:
      if not (i == text.len - 1 and text[i].isWhiteSpace):
        currentWidth += font.glyphs[text[i]].advance
      if i < text.len - 1:
        currentWidth += font.kerning[(text[i], text[i + 1])]
  lineWidths.add currentWidth
  return lineWidths.max

func wordWrapped(text: seq[Rune], font: Font, maxWidth: float32): seq[Rune] =
  var remaining: seq[seq[Rune]] = @[@[]]
  for c in text:
    if c == SPACE:
      remaining.add newSeq[Rune]()
    else:
      remaining[^1].add c
  remaining.reverse()

  var currentLine: seq[Rune]

  while remaining.len > 0:
    var currentWord = remaining.pop()
    assert not (SPACE in currentWord)

    if currentWord.len == 0:
      currentLine.add SPACE
    else:
      assert currentWord[^1] != SPACE
      # if this is the first word of the line and it is too long we need to
      # split by character
      if currentLine.len == 0 and (SPACE & currentWord).width(font) > maxWidth:
        var subWord = @[currentWord[0]]
        for c in currentWord[1 .. ^1]:
          if (subWord & c).width(font) > maxWidth:
            break
          subWord.add c
        result.add subWord & NEWLINE
        remaining.add currentWord[subWord.len .. ^1] # process rest of the word in next iteration
      else:
        if (currentLine & SPACE & currentWord).width(font) <= maxWidth:
          if currentLine.len == 0:
            currentLine = currentWord
          else:
            currentLine = currentLine & SPACE & currentWord
        else:
          result.add currentLine & NEWLINE
          remaining.add currentWord
          currentLine = @[]
  if currentLine.len > 0 and currentLine != @[SPACE]:
    result.add currentLine

  return result


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)]

  text.processedText = text.text
  if text.maxWidth > 0:
    text.processedText = text.processedText.wordWrapped(text.font, text.maxWidth / text.mesh.transform.Scaling.x)

proc `text=`*(text: var Text, newText: string) =
  `text=`(text, newText.toRunes)

proc Color*(text: Text): Vec4f =
  text.mesh.material["color", 0, Vec4f]
proc `Color=`*(text: var Text, value: Vec4f) =
  if value != text.mesh.material["color", 0, Vec4f]:
    text.mesh.material["color", 0] = value

proc HorizontalAlignment*(text: Text): HorizontalAlignment =
  text.horizontalAlignment
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 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 =
  var
    positions = newSeq[Vec3f](int(maxLen * 4))
    indices: seq[array[3, uint16]]
    uvs = newSeq[Vec2f](int(maxLen * 4))
  for i in 0 ..< maxLen:
    let offset = i * 4
    indices.add [
      [uint16(offset + 0), uint16(offset + 1), uint16(offset + 2)],
      [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)],
    ]

  result = Text(maxLen: maxLen, font: font, dirty: true, horizontalAlignment: horizontalAlignment, verticalAlignment: verticalAlignment, maxWidth: maxWidth)
  result.mesh = NewMesh(positions = positions, indices = indices, uvs = uvs, name = &"text-{instanceCounter}")
  result.mesh[].RenameAttribute("position", POSITION_ATTRIB)
  result.mesh[].RenameAttribute("uv", UV_ATTRIB)
  result.mesh.material = TEXT_MATERIAL_TYPE.InitMaterialData(
    name = font.name & " text",
    attributes = {"fontAtlas": InitDataList(@[font.fontAtlas]), "color": InitDataList(@[color])},
  )
  result.mesh.transform = transform
  `text=`(result, text)
  inc instanceCounter

  result.Refresh()