All Downloads are FREE. Search and download functionalities are using the official Maven repository.

indigo.shared.platform.DisplayObjectConversions.scala Maven / Gradle / Ivy

The newest version!
package indigo.shared.platform

import indigo.platform.assets.AtlasId
import indigo.shared.AnimationsRegister
import indigo.shared.BoundaryLocator
import indigo.shared.FontRegister
import indigo.shared.IndigoLogger
import indigo.shared.QuickCache
import indigo.shared.animation.AnimationRef
import indigo.shared.assets.AssetName
import indigo.shared.collections.Batch
import indigo.shared.config.RenderingTechnology
import indigo.shared.datatypes.FontChar
import indigo.shared.datatypes.FontInfo
import indigo.shared.datatypes.Point
import indigo.shared.datatypes.Radians
import indigo.shared.datatypes.Rectangle
import indigo.shared.datatypes.TextAlignment
import indigo.shared.datatypes.Vector2
import indigo.shared.datatypes.Vector3
import indigo.shared.datatypes.mutable.CheapMatrix4
import indigo.shared.display.DisplayCloneBatch
import indigo.shared.display.DisplayCloneTiles
import indigo.shared.display.DisplayEntity
import indigo.shared.display.DisplayGroup
import indigo.shared.display.DisplayMutants
import indigo.shared.display.DisplayObject
import indigo.shared.display.DisplayObjectUniformData
import indigo.shared.display.DisplayText
import indigo.shared.display.DisplayTextLetters
import indigo.shared.display.SpriteSheetFrame
import indigo.shared.display.SpriteSheetFrame.SpriteSheetFrameCoordinateOffsets
import indigo.shared.events.GlobalEvent
import indigo.shared.materials.ShaderData
import indigo.shared.platform.AssetMapping
import indigo.shared.scenegraph.CloneBatch
import indigo.shared.scenegraph.CloneId
import indigo.shared.scenegraph.CloneTileData
import indigo.shared.scenegraph.CloneTiles
import indigo.shared.scenegraph.DependentNode
import indigo.shared.scenegraph.EntityNode
import indigo.shared.scenegraph.Graphic
import indigo.shared.scenegraph.Group
import indigo.shared.scenegraph.Mutants
import indigo.shared.scenegraph.RenderNode
import indigo.shared.scenegraph.SceneNode
import indigo.shared.scenegraph.Shape
import indigo.shared.scenegraph.Sprite
import indigo.shared.scenegraph.Text
import indigo.shared.scenegraph.TextBox
import indigo.shared.scenegraph.TextLine
import indigo.shared.shader.ShaderPrimitive
import indigo.shared.shader.Uniform
import indigo.shared.shader.UniformBlock
import indigo.shared.time.GameTime

import scala.annotation.tailrec
import scala.scalajs.js.JSConverters._

final class DisplayObjectConversions(
    boundaryLocator: BoundaryLocator,
    animationsRegister: AnimationsRegister,
    fontRegister: FontRegister
) {

  // Per asset load
  implicit private val textureRefAndOffsetCache: QuickCache[TextureRefAndOffset]           = QuickCache.empty
  implicit private val vector2Cache: QuickCache[Vector2]                                   = QuickCache.empty
  implicit private val frameCache: QuickCache[SpriteSheetFrameCoordinateOffsets]           = QuickCache.empty
  implicit private val listDoCache: QuickCache[scalajs.js.Array[DisplayEntity]]            = QuickCache.empty
  implicit private val cloneBatchCache: QuickCache[DisplayCloneBatch]                      = QuickCache.empty
  implicit private val cloneTilesCache: QuickCache[DisplayCloneTiles]                      = QuickCache.empty
  implicit private val uniformsCache: QuickCache[scalajs.js.Array[Float]]                  = QuickCache.empty
  implicit private val textCloneTileDataCache: QuickCache[scalajs.js.Array[CloneTileData]] = QuickCache.empty
  implicit private val displayObjectCache: QuickCache[DisplayObject]                       = QuickCache.empty

  // Per frame
  implicit private val perFrameAnimCache: QuickCache[Option[AnimationRef]] = QuickCache.empty

  // Called on asset load/reload to account for atlas rebuilding etc.
  def purgeCaches(): Unit = {
    textureRefAndOffsetCache.purgeAllNow()
    vector2Cache.purgeAllNow()
    frameCache.purgeAllNow()
    listDoCache.purgeAllNow()
    cloneBatchCache.purgeAllNow()
    cloneTilesCache.purgeAllNow()
    uniformsCache.purgeAllNow()
    textCloneTileDataCache.purgeAllNow()
    displayObjectCache.purgeAllNow()
    perFrameAnimCache.purgeAllNow()
  }

  def purgeEachFrame(): Unit =
    perFrameAnimCache.purgeAllNow()

  @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
  private def lookupTexture(assetMapping: AssetMapping, name: AssetName): TextureRefAndOffset =
    QuickCache("tex-" + name.toString) {
      assetMapping.mappings
        .find(p => p._1 == name.toString)
        .map(_._2)
        .getOrElse {
          throw new Exception("Failed to find texture ref + offset for: " + name)
        }
    }

  private def cloneBatchDataToDisplayEntities(batch: CloneBatch): DisplayCloneBatch =
    if batch.staticBatchKey.isDefined then
      QuickCache(batch.staticBatchKey.get.toString) {
        new DisplayCloneBatch(
          id = batch.id,
          z = batch.depth.toDouble,
          cloneData = batch.cloneData.toJSArray
        )
      }
    else
      new DisplayCloneBatch(
        id = batch.id,
        z = batch.depth.toDouble,
        cloneData = batch.cloneData.toJSArray
      )

  private def cloneTilesDataToDisplayEntities(batch: CloneTiles): DisplayCloneTiles =
    if batch.staticBatchKey.isDefined then
      QuickCache(batch.staticBatchKey.get.toString) {
        new DisplayCloneTiles(
          id = batch.id,
          z = batch.depth.toDouble,
          cloneData = batch.cloneData.toJSArray
        )
      }
    else
      new DisplayCloneTiles(
        id = batch.id,
        z = batch.depth.toDouble,
        cloneData = batch.cloneData.toJSArray
      )

  private def mutantsToDisplayEntities(batch: Mutants): DisplayMutants =
    val uniformDataConvert: Batch[UniformBlock] => scalajs.js.Array[DisplayObjectUniformData] = uniformBlocks =>
      uniformBlocks.toJSArray.map { ub =>
        DisplayObjectUniformData(
          uniformHash = ub.uniformHash,
          blockName = ub.blockName.toString,
          data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
        )
      }

    new DisplayMutants(
      id = batch.id,
      z = batch.depth.toDouble,
      cloneData = batch.uniformBlocks.toJSArray.map(uniformDataConvert)
    )

  def processSceneNodes(
      sceneNodes: scalajs.js.Array[SceneNode],
      gameTime: GameTime,
      assetMapping: AssetMapping,
      cloneBlankDisplayObjects: => scalajs.js.Dictionary[DisplayObject],
      renderingTechnology: RenderingTechnology,
      maxBatchSize: Int,
      inputEvents: => scalajs.js.Array[GlobalEvent],
      sendEvent: GlobalEvent => Unit
  ): (scalajs.js.Array[DisplayEntity], scalajs.js.Array[(String, DisplayObject)]) =
    val f =
      sceneNodeToDisplayObject(
        gameTime,
        assetMapping,
        cloneBlankDisplayObjects,
        renderingTechnology,
        maxBatchSize,
        inputEvents,
        sendEvent
      )

    val l = sceneNodes.map { node =>
      node match
        case n: RenderNode[_] =>
          val nn = n.asInstanceOf[n.Out]
          if n.eventHandlerEnabled then
            inputEvents.foreach { e =>
              n.eventHandler((nn, e)).foreach { ee =>
                sendEvent(ee)
              }
            }

        case n: DependentNode[_] =>
          val nn = n.asInstanceOf[n.Out]
          if n.eventHandlerEnabled then
            inputEvents.foreach { e =>
              n.eventHandler((nn, e)).foreach { ee =>
                sendEvent(ee)
              }
            }

      f(node)
    }
    (l.map(_._1), l.foldLeft(scalajs.js.Array[(String, DisplayObject)]())(_ ++ _._2))

  private def groupToMatrix(group: Group): CheapMatrix4 =
    CheapMatrix4.identity
      .scale(
        if (group.flip.horizontal) -1.0 else 1.0,
        if (group.flip.vertical) -1.0 else 1.0,
        1.0f
      )
      .translate(
        -group.ref.x.toFloat,
        -group.ref.y.toFloat,
        0.0f
      )
      .scale(group.scale.x.toFloat, group.scale.y.toFloat, 1.0f)
      .rotate(group.rotation.toFloat)
      .translate(
        group.position.x.toFloat,
        group.position.y.toFloat,
        0.0f
      )

  def sceneNodeToDisplayObject(
      gameTime: GameTime,
      assetMapping: AssetMapping,
      cloneBlankDisplayObjects: => scalajs.js.Dictionary[DisplayObject],
      renderingTechnology: RenderingTechnology,
      maxBatchSize: Int,
      inputEvents: => scalajs.js.Array[GlobalEvent],
      sendEvent: GlobalEvent => Unit
  )(sceneNode: SceneNode): (DisplayEntity, scalajs.js.Array[(String, DisplayObject)]) =
    val noClones = scalajs.js.Array[(String, DisplayObject)]()
    sceneNode match {
      case x: Graphic[_] =>
        (graphicToDisplayObject(x, assetMapping), noClones)

      case s: Shape[_] =>
        (shapeToDisplayObject(s), noClones)

      case t: TextBox =>
        (textBoxToDisplayText(t), noClones)

      case s: EntityNode[_] =>
        (sceneEntityToDisplayObject(s, assetMapping), noClones)

      case c: CloneBatch =>
        (
          cloneBlankDisplayObjects.get(c.id.toString) match {
            case None =>
              DisplayGroup.empty

            case Some(_) =>
              cloneBatchDataToDisplayEntities(c)
          },
          noClones
        )

      case c: CloneTiles =>
        (
          cloneBlankDisplayObjects.get(c.id.toString) match {
            case None =>
              DisplayGroup.empty

            case Some(_) =>
              cloneTilesDataToDisplayEntities(c)
          },
          noClones
        )

      case c: Mutants =>
        (
          cloneBlankDisplayObjects.get(c.id.toString) match {
            case None =>
              DisplayGroup.empty

            case Some(_) =>
              mutantsToDisplayEntities(c)
          },
          noClones
        )

      case g: Group =>
        val children =
          processSceneNodes(
            g.children.toJSArray,
            gameTime,
            assetMapping,
            cloneBlankDisplayObjects,
            renderingTechnology,
            maxBatchSize,
            inputEvents,
            sendEvent
          )
        (
          DisplayGroup(
            groupToMatrix(g),
            g.depth.toDouble,
            children._1
          ),
          children._2
        )

      case x: Sprite[_] =>
        val animation = QuickCache("anim-" + x.bindingKey + x.animationKey + x.animationActions.hashCode.toString) {
          animationsRegister.fetchAnimationForSprite(gameTime, x.bindingKey, x.animationKey, x.animationActions)
        }

        (
          animation match {
            case None =>
              IndigoLogger.errorOnce(s"Cannot render Sprite, missing Animations with key: ${x.animationKey.toString()}")
              DisplayGroup.empty

            case Some(anim) =>
              spriteToDisplayObject(boundaryLocator, x, assetMapping, anim)
          },
          noClones
        )

      case x: Text[_] if renderingTechnology.isWebGL1 || !(x.rotation ~== Radians.zero) =>
        val alignmentOffsetX: Rectangle => Int = lineBounds =>
          x.alignment match {
            case TextAlignment.Left => 0

            case TextAlignment.Center => -(lineBounds.size.width / 2)

            case TextAlignment.Right => -lineBounds.size.width
          }

        val converterFunc: (TextLine, Int, Int) => scalajs.js.Array[DisplayEntity] =
          fontRegister
            .findByFontKey(x.fontKey)
            .map { fontInfo =>
              textLineToDisplayObjects(x, assetMapping, fontInfo)
            }
            .getOrElse { (_, _, _) =>
              IndigoLogger.errorOnce(s"Cannot render Text, missing Font with key: ${x.fontKey.toString()}")
              scalajs.js.Array()
            }

        val letters: scalajs.js.Array[DisplayEntity] =
          boundaryLocator
            .textAsLinesWithBounds(x.text, x.fontKey, x.letterSpacing, x.lineHeight)
            .toJSArray
            .foldLeft(0 -> scalajs.js.Array[DisplayEntity]()) { (acc, textLine) =>
              (
                acc._1 + textLine.lineBounds.height,
                acc._2 ++ converterFunc(textLine, alignmentOffsetX(textLine.lineBounds), acc._1)
              )
            }
            ._2

        (DisplayTextLetters(letters), noClones)

      case x: Text[_] if renderingTechnology.isWebGL2 =>
        val alignmentOffsetX: Rectangle => Int = lineBounds =>
          x.alignment match {
            case TextAlignment.Left => 0

            case TextAlignment.Center => -(lineBounds.size.width / 2)

            case TextAlignment.Right => -lineBounds.size.width
          }

        val converterFunc: (TextLine, Int, Int) => scalajs.js.Array[CloneTileData] =
          fontRegister
            .findByFontKey(x.fontKey)
            .map { fontInfo => (txtLn: TextLine, xPos: Int, yPos: Int) =>
              textLineToDisplayCloneTileData(x, fontInfo)(txtLn, xPos, yPos)
            }
            .getOrElse { (_, _, _) =>
              IndigoLogger.errorOnce(s"Cannot render Text, missing Font with key: ${x.fontKey.toString()}")
              scalajs.js.Array[CloneTileData]()
            }

        val (cloneId, clone) = makeTextCloneDisplayObject(x, assetMapping)

        val letters: scalajs.js.Array[CloneTileData] =
          boundaryLocator
            .textAsLinesWithBounds(x.text, x.fontKey, x.letterSpacing, x.lineHeight)
            .toJSArray
            .foldLeft(
              0 -> scalajs.js.Array[CloneTileData]()
            ) { (acc, textLine) =>
              (
                acc._1 + textLine.lineBounds.height,
                acc._2 ++ converterFunc(textLine, alignmentOffsetX(textLine.lineBounds), acc._1)
              )
            }
            ._2

        (
          DisplayTextLetters(
            letters.grouped(maxBatchSize).toJSArray.map { d =>
              new DisplayCloneTiles(
                id = cloneId,
                z = x.depth.toDouble,
                cloneData = d
              )
            }
          ),
          scalajs.js.Array((cloneId.toString, clone))
        )

      case _: RenderNode[_] =>
        (DisplayGroup.empty, noClones)

      case _: DependentNode[_] =>
        (DisplayGroup.empty, noClones)
    }

  def optionalAssetToOffset(assetMapping: AssetMapping, maybeAssetName: Option[AssetName]): Vector2 =
    maybeAssetName match {
      case None =>
        Vector2.zero

      case Some(assetName) =>
        lookupTexture(assetMapping, assetName).offset
    }

  def shapeToDisplayObject(leaf: Shape[?]): DisplayObject = {

    val offset = leaf match
      case s: Shape.Box =>
        val size = s.dimensions.size

        if size.width == size.height then Point.zero
        else if size.width < size.height then
          Point(-Math.round((size.height.toDouble - size.width.toDouble) / 2).toInt, 0)
        else Point(0, -Math.round((size.width.toDouble - size.height.toDouble) / 2).toInt)

      case _ =>
        Point.zero

    val boundsActual = BoundaryLocator.untransformedShapeBounds(leaf)

    val shader: ShaderData = Shape.toShaderData(leaf, boundsActual)
    val bounds             = boundsActual.toSquare

    val vec2Zero = Vector2.zero
    val uniformData: scalajs.js.Array[DisplayObjectUniformData] =
      shader.uniformBlocks.toJSArray.map { ub =>
        DisplayObjectUniformData(
          uniformHash = ub.uniformHash,
          blockName = ub.blockName.toString,
          data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
        )
      }

    val offsetRef = leaf.ref - offset

    DisplayObject(
      x = leaf.position.x.toFloat,
      y = leaf.position.y.toFloat,
      scaleX = leaf.scale.x.toFloat,
      scaleY = leaf.scale.y.toFloat,
      refX = offsetRef.x.toFloat,
      refY = offsetRef.y.toFloat,
      flipX = if leaf.flip.horizontal then -1.0 else 1.0,
      flipY = if leaf.flip.vertical then -1.0 else 1.0,
      rotation = leaf.rotation,
      z = leaf.depth.toDouble,
      width = bounds.size.width,
      height = bounds.size.height,
      atlasName = None,
      frame = SpriteSheetFrame.defaultOffset,
      channelOffset1 = vec2Zero,
      channelOffset2 = vec2Zero,
      channelOffset3 = vec2Zero,
      texturePosition = vec2Zero,
      textureSize = vec2Zero,
      atlasSize = vec2Zero,
      shaderId = shader.shaderId,
      shaderUniformData = uniformData
    )
  }

  private given CanEqual[Option[TextureRefAndOffset], Option[TextureRefAndOffset]] = CanEqual.derived

  def sceneEntityToDisplayObject(leaf: EntityNode[?], assetMapping: AssetMapping): DisplayObject = {
    val shader: ShaderData = leaf.toShaderData

    val channelOffset1 = optionalAssetToOffset(assetMapping, shader.channel1)
    val channelOffset2 = optionalAssetToOffset(assetMapping, shader.channel2)
    val channelOffset3 = optionalAssetToOffset(assetMapping, shader.channel3)

    val bounds = Rectangle(Point.zero, leaf.size)

    val texture =
      shader.channel0.map(assetName => lookupTexture(assetMapping, assetName))

    val frameInfo: SpriteSheetFrameCoordinateOffsets =
      texture match {
        case None =>
          SpriteSheetFrame.defaultOffset

        case Some(texture) =>
          QuickCache(s"${bounds.hashCode().toString}_${shader.hashCode().toString}") {
            SpriteSheetFrame.calculateFrameOffset(
              atlasSize = texture.atlasSize,
              frameCrop = bounds,
              textureOffset = texture.offset
            )
          }
      }

    val shaderId = shader.shaderId

    val uniformData: scalajs.js.Array[DisplayObjectUniformData] =
      shader.uniformBlocks.toJSArray.map { ub =>
        DisplayObjectUniformData(
          uniformHash = ub.uniformHash,
          blockName = ub.blockName.toString,
          data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
        )
      }

    DisplayObject(
      x = leaf.position.x.toFloat,
      y = leaf.position.y.toFloat,
      scaleX = leaf.scale.x.toFloat,
      scaleY = leaf.scale.y.toFloat,
      refX = leaf.ref.x.toFloat,
      refY = leaf.ref.y.toFloat,
      flipX = if leaf.flip.horizontal then -1.0 else 1.0,
      flipY = if leaf.flip.vertical then -1.0 else 1.0,
      rotation = leaf.rotation,
      z = leaf.depth.toDouble,
      width = bounds.size.width,
      height = bounds.size.height,
      atlasName = texture.map(_.atlasName),
      frame = frameInfo,
      channelOffset1 = frameInfo.offsetToCoords(channelOffset1),
      channelOffset2 = frameInfo.offsetToCoords(channelOffset2),
      channelOffset3 = frameInfo.offsetToCoords(channelOffset3),
      texturePosition = texture.map(_.offset).getOrElse(Vector2.zero),
      textureSize = texture.map(_.size).getOrElse(Vector2.zero),
      atlasSize = texture.map(_.atlasSize).getOrElse(Vector2.zero),
      shaderId = shaderId,
      shaderUniformData = uniformData
    )
  }

  def textBoxToDisplayText(leaf: TextBox): DisplayText =
    DisplayText(
      text = leaf.text,
      style = leaf.style,
      x = leaf.position.x.toFloat,
      y = leaf.position.y.toFloat,
      scaleX = leaf.scale.x.toFloat,
      scaleY = leaf.scale.y.toFloat,
      refX = leaf.ref.x.toFloat,
      refY = leaf.ref.y.toFloat,
      flipX = if leaf.flip.horizontal then -1.0 else 1.0,
      flipY = if leaf.flip.vertical then -1.0 else 1.0,
      rotation = leaf.rotation,
      z = leaf.depth.toDouble,
      width = leaf.size.width,
      height = leaf.size.height
    )

  def graphicToDisplayObject(leaf: Graphic[?], assetMapping: AssetMapping): DisplayObject = {
    val shaderData     = leaf.material.toShaderData
    val shaderDataHash = shaderData.toCacheKey
    val materialName   = shaderData.channel0.get

    val emissiveOffset = findAssetOffsetValues(assetMapping, shaderData.channel1, shaderDataHash, "_e")
    val normalOffset   = findAssetOffsetValues(assetMapping, shaderData.channel2, shaderDataHash, "_n")
    val specularOffset = findAssetOffsetValues(assetMapping, shaderData.channel3, shaderDataHash, "_s")

    val texture = lookupTexture(assetMapping, materialName)

    val frameInfo =
      QuickCache(s"${leaf.crop.hashCode().toString}_$shaderDataHash") {
        SpriteSheetFrame.calculateFrameOffset(
          atlasSize = texture.atlasSize,
          frameCrop = leaf.crop,
          textureOffset = texture.offset
        )
      }

    val shaderId = shaderData.shaderId

    val uniformData: scalajs.js.Array[DisplayObjectUniformData] =
      shaderData.uniformBlocks.toJSArray.map { ub =>
        DisplayObjectUniformData(
          uniformHash = ub.uniformHash,
          blockName = ub.blockName.toString,
          data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
        )
      }

    DisplayObject(
      x = leaf.position.x.toFloat,
      y = leaf.position.y.toFloat,
      scaleX = leaf.scale.x.toFloat,
      scaleY = leaf.scale.y.toFloat,
      refX = leaf.ref.x.toFloat,
      refY = leaf.ref.y.toFloat,
      flipX = if leaf.flip.horizontal then -1.0 else 1.0,
      flipY = if leaf.flip.vertical then -1.0 else 1.0,
      rotation = leaf.rotation,
      z = leaf.depth.toDouble,
      width = leaf.crop.size.width,
      height = leaf.crop.size.height,
      atlasName = Some(texture.atlasName),
      frame = frameInfo,
      channelOffset1 = frameInfo.offsetToCoords(emissiveOffset),
      channelOffset2 = frameInfo.offsetToCoords(normalOffset),
      channelOffset3 = frameInfo.offsetToCoords(specularOffset),
      texturePosition = texture.offset,
      textureSize = texture.size,
      atlasSize = texture.atlasSize,
      shaderId = shaderId,
      shaderUniformData = uniformData
    )
  }

  def spriteToDisplayObject(
      boundaryLocator: BoundaryLocator,
      leaf: Sprite[?],
      assetMapping: AssetMapping,
      anim: AnimationRef
  ): DisplayObject = {
    val material       = leaf.material
    val shaderData     = material.toShaderData
    val shaderDataHash = shaderData.toCacheKey
    val materialName   = shaderData.channel0.get

    val emissiveOffset = findAssetOffsetValues(assetMapping, shaderData.channel1, shaderDataHash, "_e")
    val normalOffset   = findAssetOffsetValues(assetMapping, shaderData.channel2, shaderDataHash, "_n")
    val specularOffset = findAssetOffsetValues(assetMapping, shaderData.channel3, shaderDataHash, "_s")

    val texture = lookupTexture(assetMapping, materialName)

    val frameInfo =
      QuickCache(anim.frameHash + shaderDataHash) {
        SpriteSheetFrame.calculateFrameOffset(
          atlasSize = texture.atlasSize,
          frameCrop = anim.currentFrame.crop,
          textureOffset = texture.offset
        )
      }

    val bounds = boundaryLocator.spriteFrameBounds(leaf).getOrElse(Rectangle.zero)

    val shaderId = shaderData.shaderId

    val uniformData: scalajs.js.Array[DisplayObjectUniformData] =
      shaderData.uniformBlocks.toJSArray.map { ub =>
        DisplayObjectUniformData(
          uniformHash = ub.uniformHash,
          blockName = ub.blockName.toString,
          data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
        )
      }

    DisplayObject(
      x = leaf.position.x.toFloat,
      y = leaf.position.y.toFloat,
      scaleX = leaf.scale.x.toFloat,
      scaleY = leaf.scale.y.toFloat,
      refX = leaf.ref.x.toFloat,
      refY = leaf.ref.y.toFloat,
      flipX = if leaf.flip.horizontal then -1.0 else 1.0,
      flipY = if leaf.flip.vertical then -1.0 else 1.0,
      rotation = leaf.rotation,
      z = leaf.depth.toDouble,
      width = bounds.width,
      height = bounds.height,
      atlasName = Some(texture.atlasName),
      frame = frameInfo,
      channelOffset1 = frameInfo.offsetToCoords(emissiveOffset),
      channelOffset2 = frameInfo.offsetToCoords(normalOffset),
      channelOffset3 = frameInfo.offsetToCoords(specularOffset),
      texturePosition = texture.offset,
      textureSize = texture.size,
      atlasSize = texture.atlasSize,
      shaderId = shaderId,
      shaderUniformData = uniformData
    )
  }

  def textLineToDisplayObjects(
      leaf: Text[?],
      assetMapping: AssetMapping,
      fontInfo: FontInfo
  ): (TextLine, Int, Int) => scalajs.js.Array[DisplayEntity] =
    (line, alignmentOffsetX, yOffset) => {

      val material       = leaf.material
      val shaderData     = material.toShaderData
      val shaderDataHash = shaderData.toCacheKey
      val materialName   = shaderData.channel0.get

      val lineHash: String =
        "[indigo_txt]" +
          leaf.material.hashCode.toString +
          leaf.position.hashCode.toString +
          leaf.scale.hashCode.toString +
          leaf.rotation.hashCode.toString +
          leaf.ref.hashCode.toString +
          leaf.flip.horizontal.toString +
          leaf.flip.vertical.toString +
          leaf.depth.toString +
          leaf.fontKey.toString +
          line.hashCode.toString

      val emissiveOffset = findAssetOffsetValues(assetMapping, shaderData.channel1, shaderDataHash, "_e")
      val normalOffset   = findAssetOffsetValues(assetMapping, shaderData.channel2, shaderDataHash, "_n")
      val specularOffset = findAssetOffsetValues(assetMapping, shaderData.channel3, shaderDataHash, "_s")

      val texture = lookupTexture(assetMapping, materialName)

      val shaderId = shaderData.shaderId

      val uniformData: scalajs.js.Array[DisplayObjectUniformData] =
        shaderData.uniformBlocks.toJSArray.map { ub =>
          DisplayObjectUniformData(
            uniformHash = ub.uniformHash,
            blockName = ub.blockName.toString,
            data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
          )
        }

      QuickCache(lineHash) {
        zipWithCharDetails(line.text.toArray, fontInfo, leaf.letterSpacing).map { case (fontChar, xPosition) =>
          val frameInfo =
            QuickCache(fontChar.bounds.hashCode().toString + "_" + shaderDataHash) {
              SpriteSheetFrame.calculateFrameOffset(
                atlasSize = texture.atlasSize,
                frameCrop = fontChar.bounds,
                textureOffset = texture.offset
              )
            }

          DisplayObject(
            x = leaf.position.x.toFloat,
            y = leaf.position.y.toFloat,
            scaleX = leaf.scale.x.toFloat,
            scaleY = leaf.scale.y.toFloat,
            refX = (leaf.ref.x + -(xPosition + alignmentOffsetX)).toFloat,
            refY = (leaf.ref.y - yOffset).toFloat,
            flipX = if leaf.flip.horizontal then -1.0 else 1.0,
            flipY = if leaf.flip.vertical then -1.0 else 1.0,
            rotation = leaf.rotation,
            z = leaf.depth.toDouble,
            width = fontChar.bounds.width,
            height = fontChar.bounds.height,
            atlasName = Some(texture.atlasName),
            frame = frameInfo,
            channelOffset1 = frameInfo.offsetToCoords(emissiveOffset),
            channelOffset2 = frameInfo.offsetToCoords(normalOffset),
            channelOffset3 = frameInfo.offsetToCoords(specularOffset),
            texturePosition = texture.offset,
            textureSize = texture.size,
            atlasSize = texture.atlasSize,
            shaderId = shaderId,
            shaderUniformData = uniformData
          )
        }
      }
    }

  def makeTextCloneDisplayObject(
      leaf: Text[?],
      assetMapping: AssetMapping
  ): (CloneId, DisplayObject) = {

    val cloneId: CloneId =
      CloneId(
        "[indigo_txt_clone]" +
          leaf.material.hashCode.toString +
          leaf.position.hashCode.toString +
          leaf.scale.hashCode.toString +
          leaf.rotation.hashCode.toString +
          leaf.ref.hashCode.toString +
          leaf.flip.horizontal.toString +
          leaf.flip.vertical.toString +
          leaf.depth.toString +
          leaf.fontKey.toString
      )

    val clone =
      QuickCache(s"[indigo_text_clone_ref][${cloneId.toString}]") {
        val material       = leaf.material
        val shaderData     = material.toShaderData
        val shaderDataHash = shaderData.toCacheKey
        val materialName   = shaderData.channel0.get
        val emissiveOffset = findAssetOffsetValues(assetMapping, shaderData.channel1, shaderDataHash, "_e")
        val normalOffset   = findAssetOffsetValues(assetMapping, shaderData.channel2, shaderDataHash, "_n")
        val specularOffset = findAssetOffsetValues(assetMapping, shaderData.channel3, shaderDataHash, "_s")
        val texture        = lookupTexture(assetMapping, materialName)
        val shaderId       = shaderData.shaderId

        val uniformData: scalajs.js.Array[DisplayObjectUniformData] =
          shaderData.uniformBlocks.toJSArray.map { ub =>
            DisplayObjectUniformData(
              uniformHash = ub.uniformHash,
              blockName = ub.blockName.toString,
              data = DisplayObjectConversions.packUBO(ub.uniforms, ub.uniformHash, false)
            )
          }

        val frameInfo =
          SpriteSheetFrame.calculateFrameOffset(
            atlasSize = texture.atlasSize,
            frameCrop = Rectangle.one,
            textureOffset = texture.offset
          )

        DisplayObject(
          x = leaf.position.x.toFloat,
          y = leaf.position.y.toFloat,
          scaleX = leaf.scale.x.toFloat,
          scaleY = leaf.scale.y.toFloat,
          refX = leaf.ref.x.toFloat,
          refY = leaf.ref.y.toFloat,
          flipX = if leaf.flip.horizontal then -1.0 else 1.0,
          flipY = if leaf.flip.vertical then -1.0 else 1.0,
          rotation = leaf.rotation,
          z = leaf.depth.toDouble,
          width = 1,
          height = 1,
          atlasName = Some(texture.atlasName),
          frame = frameInfo,
          channelOffset1 = frameInfo.offsetToCoords(emissiveOffset),
          channelOffset2 = frameInfo.offsetToCoords(normalOffset),
          channelOffset3 = frameInfo.offsetToCoords(specularOffset),
          texturePosition = texture.offset,
          textureSize = texture.size,
          atlasSize = texture.atlasSize,
          shaderId = shaderId,
          shaderUniformData = uniformData
        )
      }

    (
      cloneId,
      clone
    )
  }

  def textLineToDisplayCloneTileData(
      leaf: Text[?],
      fontInfo: FontInfo
  ): (TextLine, Int, Int) => scalajs.js.Array[CloneTileData] =
    (line, alignmentOffsetX, yOffset) => {
      val lineHash: String =
        "[indigo_tln]" +
          leaf.position.hashCode.toString +
          leaf.ref.hashCode.toString +
          leaf.scale.hashCode.toString +
          line.hashCode.toString +
          leaf.fontKey.toString

      QuickCache(lineHash) {
        zipWithCharDetails(line.text.toArray, fontInfo, leaf.letterSpacing).map { case (fontChar, xPosition) =>
          CloneTileData(
            x = leaf.position.x + leaf.ref.x + xPosition + alignmentOffsetX,
            y = leaf.position.y + leaf.ref.y + yOffset,
            rotation = Radians.zero,
            scaleX = leaf.scale.x.toFloat,
            scaleY = leaf.scale.y.toFloat,
            cropX = fontChar.bounds.x,
            cropY = fontChar.bounds.y,
            cropWidth = fontChar.bounds.width,
            cropHeight = fontChar.bounds.height
          )
        }
      }
    }

  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var accCharDetails: scalajs.js.Array[(FontChar, Int)] = new scalajs.js.Array()

  private def zipWithCharDetails(
      charList: Array[Char],
      fontInfo: FontInfo,
      letterSpacing: Int
  ): scalajs.js.Array[(FontChar, Int)] = {
    @tailrec
    def rec(remaining: scalajs.js.Array[(Char, FontChar)], nextX: Int): scalajs.js.Array[(FontChar, Int)] =
      if remaining.isEmpty then accCharDetails
      else
        val x  = remaining.head
        val xs = remaining.tail
        (x._2, nextX) +=: accCharDetails

        val ls = if xs.isEmpty then 0 else letterSpacing
        rec(xs, nextX + x._2.bounds.width + ls)

    accCharDetails = new scalajs.js.Array()
    rec(charList.toJSArray.map(c => (c, fontInfo.findByCharacter(c))), 0)
  }

  def findAssetOffsetValues(
      assetMapping: AssetMapping,
      maybeAssetName: Option[AssetName],
      cacheKey: String,
      cacheSuffix: String
  ): Vector2 =
    QuickCache[Vector2](cacheKey + cacheSuffix) {
      maybeAssetName
        .map { t =>
          lookupTexture(assetMapping, t).offset
        }
        .getOrElse(Vector2.zero)
    }

  extension (sd: ShaderData)
    def toCacheKey: String =
      sd.shaderId.toString +
        sd.channel0.map(_.toString).getOrElse("") +
        sd.channel1.map(_.toString).getOrElse("") +
        sd.channel2.map(_.toString).getOrElse("") +
        sd.channel3.map(_.toString).getOrElse("") +
        sd.uniformBlocks.map(_.uniformHash).mkString
}

object DisplayObjectConversions {

  private val empty0: scalajs.js.Array[Float] = scalajs.js.Array[Float]()
  private val empty1: scalajs.js.Array[Float] = scalajs.js.Array[Float](0.0f)
  private val empty2: scalajs.js.Array[Float] = scalajs.js.Array[Float](0.0f, 0.0f)
  private val empty3: scalajs.js.Array[Float] = scalajs.js.Array[Float](0.0f, 0.0f, 0.0f)

  def expandTo4(arr: scalajs.js.Array[Float]): scalajs.js.Array[Float] =
    arr.length match {
      case 0 => arr
      case 1 => arr ++ empty3
      case 2 => arr ++ empty2
      case 3 => arr ++ empty1
      case 4 => arr
      case _ => arr
    }

  // takes a list because only converted to JSArray if value not cached.
  def packUBO(
      uniforms: Batch[(Uniform, ShaderPrimitive)],
      cacheKey: String,
      disableCache: Boolean
  )(using QuickCache[scalajs.js.Array[Float]]): scalajs.js.Array[Float] = {
    @tailrec
    def rec(
        remaining: scalajs.js.Array[ShaderPrimitive],
        current: scalajs.js.Array[Float],
        acc: scalajs.js.Array[Float]
    ): scalajs.js.Array[Float] =
      remaining match
        case us if us.isEmpty =>
          // println(s"done, expanded: ${current.toList} to ${expandTo4(current).toList}")
          // println(s"result: ${(acc ++ expandTo4(current)).toList}")
          acc ++ expandTo4(current)

        case us if current.length == 4 =>
          // println(s"current full, sub-result: ${(acc ++ current).toList}")
          rec(us, empty0, acc ++ current)

        case us if current.isEmpty && us.head.isArray =>
          // println(s"Found an array, current is empty, set current to: ${u.toArray.toList}")
          rec(us.tail, us.head.toJSArray, acc)

        case us if current.length == 1 && us.head.length == 2 =>
          // println("Current value is float, must not straddle byte boundary when adding vec2")
          rec(us.tail, current ++ scalajs.js.Array(0.0f) ++ us.head.toJSArray, acc)

        case us if current.length + us.head.length > 4 =>
          // println(s"doesn't fit, expanded: ${current.toList} to ${expandTo4(current).toList},  sub-result: ${(acc ++ expandTo4(current)).toList}")
          rec(us, empty0, acc ++ expandTo4(current))

        case us if us.head.isArray =>
          // println(s"fits but next value is array, expanded: ${current.toList} to ${expandTo4(current).toList},  sub-result: ${(acc ++ expandTo4(current)).toList}")
          rec(us, empty0, acc ++ current)

        case us =>
          // println(s"fits, current is now: ${(current ++ u.toArray).toList}")
          rec(us.tail, current ++ us.head.toJSArray, acc)

    QuickCache(cacheKey, disableCache) {
      rec(uniforms.toJSArray.map(_._2), empty0, empty0)
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy