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

indigo.shared.BoundaryLocator.scala Maven / Gradle / Ivy

The newest version!
package indigo.shared

import indigo.platform.assets.DynamicText
import indigo.shared.collections.Batch
import indigo.shared.datatypes.FontInfo
import indigo.shared.datatypes.FontKey
import indigo.shared.datatypes.Point
import indigo.shared.datatypes.Rectangle
import indigo.shared.datatypes.Size
import indigo.shared.datatypes.TextAlignment
import indigo.shared.datatypes.Vector3
import indigo.shared.datatypes.mutable.CheapMatrix4
import indigo.shared.scenegraph.CloneBatch
import indigo.shared.scenegraph.CloneTiles
import indigo.shared.scenegraph.EntityNode
import indigo.shared.scenegraph.Graphic
import indigo.shared.scenegraph.Group
import indigo.shared.scenegraph.Mutants
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

final class BoundaryLocator(
    animationsRegister: AnimationsRegister,
    fontRegister: FontRegister,
    dynamicText: DynamicText
):

  implicit private val maybeBoundsCache: QuickCache[Option[Rectangle]]      = QuickCache.empty
  implicit private val boundsCache: QuickCache[Rectangle]                   = QuickCache.empty
  implicit private val textLinesCache: QuickCache[Batch[TextLine]]          = QuickCache.empty
  implicit private val textAllLineBoundsCache: QuickCache[Array[Rectangle]] = QuickCache.empty

  private[indigo] def purgeCache(): Unit = {
    maybeBoundsCache.purgeAllNow()
    boundsCache.purgeAllNow()
    textLinesCache.purgeAllNow()
    textAllLineBoundsCache.purgeAllNow()
  }

  /** Measures the size of a `TextBox` using the browsers canvas APIs. This is a slow operation.
    */
  def measureText(textBox: TextBox): Rectangle =
    val rect =
      dynamicText
        .measureText(
          textBox.text,
          textBox.style,
          textBox.size.width,
          textBox.size.height
        )
        .moveTo(textBox.position)

    BoundaryLocator.findBounds(textBox, rect.position, rect.size, textBox.ref)

  /** Safely finds the bounds of any given scene node, if the node has bounds. It is not possible to sensibly measure
    * the bounds of some node types, such as clones, and some nodes are dependant on external data that may be missing.
    */
  def findBounds(sceneNode: SceneNode): Option[Rectangle] =
    sceneNode match {
      case s: Shape[_] =>
        Option(BoundaryLocator.findShapeBounds(s))

      case g: Graphic[_] =>
        Option(g.bounds)

      case t: TextBox =>
        Option(t.bounds)

      case s: EntityNode[_] =>
        Option(BoundaryLocator.findBounds(s, s.position, s.size, s.ref))

      case g: Group =>
        Option(groupBounds(g))

      case _: CloneBatch =>
        None

      case _: CloneTiles =>
        None

      case _: Mutants =>
        None

      case s: Sprite[_] =>
        spriteBounds(s)

      case t: Text[_] =>
        Option(textBounds(t))

      case _ =>
        None
    }

  /** Finds the bounds or returns a `Rectangle` of size zero for convenience.
    */
  def bounds(sceneNode: SceneNode): Rectangle =
    findBounds(sceneNode).getOrElse(Rectangle.zero)

  private def groupBounds(group: Group): Rectangle =
    val rect =
      if group.children.isEmpty then Rectangle.zero
      else
        group.children.tail
          .foldLeft(findBounds(group.children.head)) { (acc, node) =>
            (acc, findBounds(node)) match
              case (Some(a), Some(b)) => Option(Rectangle.expandToInclude(a, b))
              case (r @ Some(_), _)   => r
              case (_, r @ Some(_))   => r
              case (r, _)             => r
          }
          .map(_.moveBy(group.position))
          .getOrElse(Rectangle.zero)

    BoundaryLocator.findBounds(group, rect.position, rect.size, group.ref)
  end groupBounds

  def spriteFrameBounds(sprite: Sprite[?]): Option[Rectangle] =
    QuickCache(s"""sprite-${sprite.bindingKey.toString}-${sprite.animationKey.toString}""") {
      animationsRegister.fetchAnimationInLastState(sprite.bindingKey, sprite.animationKey) match {
        case Some(animation) =>
          Option(Rectangle(sprite.position, animation.currentFrame.crop.size))

        case None =>
          IndigoLogger.errorOnce(s"Cannot build bounds for Sprite with bindingKey: ${sprite.bindingKey.toString()}")
          None
      }
    }

  private def spriteBounds(sprite: Sprite[?]): Option[Rectangle] =
    spriteFrameBounds(sprite).map(rect => BoundaryLocator.findBounds(sprite, rect.position, rect.size, sprite.ref))

  // Text / Fonts

  def textLineBounds(lineText: String, fontInfo: FontInfo, letterSpacing: Int, lineHeight: Int): Rectangle =
    QuickCache(s"""textline-${fontInfo.fontKey}-$lineText-${letterSpacing.toString()}-${lineHeight.toString()}""") {
      if lineText.isEmpty then {
        val b = fontInfo.findByCharacter(' ').bounds
        b.withSize(Size(0, b.height + lineHeight))
      } else {
        val b =
          lineText
            .toCharArray()
            .map(c => fontInfo.findByCharacter(c).bounds)
            .foldLeft(Rectangle.zero) { (acc, curr) =>
              Rectangle(0, 0, acc.width + curr.width + letterSpacing, Math.max(acc.height, curr.height + lineHeight))
            }
        b.withSize(b.size - Size(letterSpacing, 0))
      }

    }

  def textAsLinesWithBounds(text: String, fontKey: FontKey, letterSpacing: Int, lineHeight: Int): Batch[TextLine] =
    QuickCache(s"""text-lines-$fontKey-$text-${letterSpacing.toString()}-${lineHeight.toString()}""") {
      fontRegister
        .findByFontKey(fontKey)
        .map { fontInfo =>
          text.linesIterator.toList
            .map(lineText => new TextLine(lineText, textLineBounds(lineText, fontInfo, letterSpacing, lineHeight)))
            .foldLeft((0, Batch.empty[TextLine])) { case ((yPos, lines), textLine) =>
              (yPos + textLine.lineBounds.height, lines ++ Batch(textLine.moveTo(0, yPos)))
            }
            ._2
        }
        .getOrElse {
          IndigoLogger.errorOnce(s"Cannot build Text lines, missing Font with key: ${fontKey.toString()}")
          Batch.empty
        }
    }

  def textAllLineBounds(text: String, fontKey: FontKey, letterSpacing: Int, lineHeight: Int): Array[Rectangle] =
    QuickCache(s"""text-all-line-bounds-$fontKey-$text-${letterSpacing.toString()}-${lineHeight.toString()}""") {
      fontRegister
        .findByFontKey(fontKey)
        .map { fontInfo =>
          text.linesIterator.toArray
            .map(lineText => textLineBounds(lineText, fontInfo, letterSpacing, lineHeight))
            .foldLeft((0, Array[Rectangle]())) { case ((yPos, lines), lineBounds) =>
              (yPos + lineBounds.height, lines ++ Array(lineBounds.moveTo(0, yPos)))
            }
            ._2
        }
        .getOrElse {
          IndigoLogger.errorOnce(s"Cannot build Text line bounds, missing Font with key: ${fontKey.toString()}")
          Array()
        }
    }

  def textBounds(text: Text[?]): Rectangle =
    val unaligned =
      textAllLineBounds(text.text, text.fontKey, text.letterSpacing, text.lineHeight)
        .fold(Rectangle.zero) { (acc, next) =>
          acc.resize(Size(Math.max(acc.width, next.width), acc.height + next.height))
        }

    val rect =
      unaligned.moveTo(text.position)

    val offset: Int =
      text.alignment match {
        case TextAlignment.Left   => 0
        case TextAlignment.Center => rect.size.width / 2
        case TextAlignment.Right  => rect.size.width
      }

    BoundaryLocator.findBounds(text, rect.position, rect.size, text.ref + Point(offset, 0))

object BoundaryLocator:
  def findBounds(entity: SceneNode, position: Point, size: Size, ref: Point): Rectangle =
    val m =
      CheapMatrix4.identity
        .translate(-ref.x.toFloat, -ref.y.toFloat, 0.0f)
        .rotate(entity.rotation.toFloat)
        .scale(entity.scale.x.toFloat, entity.scale.y.toFloat, 1.0f)
        .translate(position.x.toFloat, position.y.toFloat, 0.0f)

    Rectangle.fromPoints(
      m.transform(Vector3(0, 0, 0)).toPoint,
      m.transform(Vector3(size.width, 0, 0)).toPoint,
      m.transform(Vector3(size.width, size.height, 0)).toPoint,
      m.transform(Vector3(0, size.height, 0)).toPoint
    )

  def untransformedShapeBounds(shape: Shape[?]): Rectangle =
    shape match
      case s: Shape.Box =>
        Rectangle(
          (s.dimensions.position - (s.stroke.width / 2)),
          s.dimensions.size + s.stroke.width
        )

      case s: Shape.Circle =>
        Rectangle(
          s.position,
          Size(s.circle.radius * 2) + s.stroke.width
        )

      case s: Shape.Line =>
        Rectangle(s.position, s.size)

      case s: Shape.Polygon =>
        val ex = if s.stroke.width == 1 then 1 else s.stroke.width / 2
        Rectangle.fromPointCloud(s.vertices).expand(ex)

  def findShapeBounds(shape: Shape[?]): Rectangle =
    val rect = untransformedShapeBounds(shape)
    findBounds(shape, rect.position, rect.size, shape.ref)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy