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

indigo.shared.geometry.BoundingCircle.scala Maven / Gradle / Ivy

The newest version!
package indigo.shared.geometry

import indigo.shared.collections.Batch
import indigo.shared.datatypes.Circle
import indigo.shared.datatypes.Point
import indigo.shared.datatypes.Rectangle
import indigo.shared.datatypes.Size
import indigo.shared.datatypes.Vector2

final case class BoundingCircle(position: Vertex, radius: Double) derives CanEqual:
  lazy val x: Double        = position.x
  lazy val y: Double        = position.y
  lazy val diameter: Double = radius * 2

  lazy val left: Double   = x - radius
  lazy val right: Double  = x + radius
  lazy val top: Double    = y - radius
  lazy val bottom: Double = y + radius

  lazy val center: Vertex = position

  def contains(vertex: Vertex): Boolean =
    vertex.distanceTo(position) <= radius
  def contains(x: Double, y: Double): Boolean =
    contains(Vertex(x, y))
  def contains(vector: Vector2): Boolean =
    contains(Vertex.fromVector2(vector))

  def +(d: Double): BoundingCircle = resize(radius + d)
  def -(d: Double): BoundingCircle = resize(radius - d)
  def *(d: Double): BoundingCircle = resize(radius * d)
  def /(d: Double): BoundingCircle = resize(radius / d)

  def sdf(vertex: Vertex): Double =
    BoundingCircle.signedDistanceFunction(vertex - position, radius)
  def sdf(vector: Vector2): Double =
    sdf(Vertex.fromVector2(vector))

  def distanceToBoundary(vertex: Vertex): Double =
    sdf(vertex)
  def distanceToBoundary(vector: Vector2): Double =
    sdf(Vertex.fromVector2(vector))

  def expandToInclude(other: BoundingCircle): BoundingCircle =
    BoundingCircle.expandToInclude(this, other)

  def encompasses(other: BoundingCircle): Boolean =
    BoundingCircle.encompassing(this, other)
  def encompasses(other: BoundingBox): Boolean =
    BoundingCircle.encompassing(this, other)

  def overlaps(other: BoundingCircle): Boolean =
    BoundingCircle.overlapping(this, other)
  def overlaps(other: BoundingBox): Boolean =
    BoundingCircle.overlapping(this, other)
  def overlaps(other: LineSegment): Boolean =
    contains(other.start) || contains(other.end) || lineIntersects(other)

  def moveBy(amount: Vertex): BoundingCircle =
    this.copy(position = position + amount)
  def moveBy(x: Double, y: Double): BoundingCircle =
    moveBy(Vertex(x, y))
  def moveBy(amount: Vector2): BoundingCircle =
    moveBy(Vertex.fromVector2(amount))

  def moveTo(newPosition: Vertex): BoundingCircle =
    this.copy(position = newPosition)
  def moveTo(x: Double, y: Double): BoundingCircle =
    moveTo(Vertex(x, y))
  def moveTo(newPosition: Vector2): BoundingCircle =
    moveTo(Vertex.fromVector2(newPosition))

  def resize(newRadius: Double): BoundingCircle =
    this.copy(radius = newRadius)
  def resizeTo(newRadius: Double): BoundingCircle =
    resize(newRadius)
  def resizeBy(amount: Double): BoundingCircle =
    expand(amount)
  def withRadius(newRadius: Double): BoundingCircle =
    resize(newRadius)
  def expand(by: Double): BoundingCircle =
    resize(radius + by)
  def contract(by: Double): BoundingCircle =
    resize(radius - by)

  def toCircle: Circle =
    Circle(position.toPoint, radius.toInt)

  @deprecated("Please use `toIncircleRectangle`, or alternatively `toCircumcircleRectangle`.")
  def toRectangle: Rectangle =
    Rectangle.fromIncircle(this.toCircle)
  def toIncircleRectangle: Rectangle =
    Rectangle.fromIncircle(this.toCircle)
  def toCircumcircleRectangle: Rectangle =
    Rectangle.fromCircumcircle(this.toCircle)

  @deprecated("Please use `toIncircleBoundingBox`, or alternatively `toCircumcircleBoundingBox`.")
  def toBoundingBox: BoundingBox =
    BoundingBox.fromIncircle(this)
  def toIncircleBoundingBox: BoundingBox =
    BoundingBox.fromIncircle(this)
  def toCircumcircleBoundingBox: BoundingBox =
    BoundingBox.fromCircumcircle(this)

  def lineIntersects(line: LineSegment): Boolean =
    BoundingCircle.lineIntersects(this, line)

  def lineIntersectsAt(line: LineSegment): BoundingCircleLineIntersect =
    BoundingCircle.lineIntersectsAt(this, line)

  /** Reflects the incoming 'ray' off of the BoundingCircle.
    */
  def reflect(ray: LineSegment): Option[ReflectionData] =
    lineIntersectsAt(ray).nearest.map { at =>
      val nrml      = (at - center).toVector2.normalise
      val incident  = (at - ray.start).toVector2.normalise
      val reflected = (incident - nrml * (2.0 * incident.dot(nrml))).normalise

      ReflectionData(
        at,
        nrml,
        incident,
        reflected
      )
    }

  def ~==(other: BoundingCircle): Boolean =
    (position ~== other.position) && Math.abs(radius - other.radius) < 0.0001

object BoundingCircle:

  val zero: BoundingCircle =
    BoundingCircle(Vertex.zero, 0)

  def apply(x: Double, y: Double, radius: Double): BoundingCircle =
    BoundingCircle(Vertex(x, y), radius)

  /** Creates a `BoundingCircle` from two vertices where the first represents the center of the circle and the second is
    * used to calculate the radius by measuring the distance to the center.
    */
  def fromTwoVertices(center: Vertex, boundary: Vertex): BoundingCircle =
    BoundingCircle(center, center.distanceTo(boundary))

  /** Creates a `BoundingCircle` from three vertices such that all of the vertices lie on the circles circumference.
    */
  def fromThreeVertices(a: Vertex, b: Vertex, c: Vertex): Option[BoundingCircle] =
    // Sides
    val sideA = a.distanceTo(b)
    val sideB = b.distanceTo(c)
    val sideC = c.distanceTo(a)

    // Find the three angles from the sides using the law of cosine
    val angleA = Math.acos((Math.pow(sideB, 2) + Math.pow(sideC, 2) - Math.pow(sideA, 2)) / (2 * sideB * sideC))
    val angleB = Math.acos((Math.pow(sideC, 2) + Math.pow(sideA, 2) - Math.pow(sideB, 2)) / (2 * sideC * sideA))
    val angleC = Math.acos((Math.pow(sideA, 2) + Math.pow(sideB, 2) - Math.pow(sideC, 2)) / (2 * sideA * sideB))

    // Then find the widest angle, the point there connects to the other two
    List(angleA -> c, angleB -> a, angleC -> b).sortBy(_._1).map(_._2) match
      case vtxC :: vtxB :: vtxA :: Nil =>
        // To form two `LineSegments`
        val lsA = LineSegment(vtxA, vtxB)
        val lsB = LineSegment(vtxA, vtxC)

        // We then take a normal from the center of the line segment
        val lineA = LineSegment(lsA.center, lsA.center + lsA.normal).toLine
        val lineB = LineSegment(lsB.center, lsB.center + lsB.normal).toLine

        // Where the two normal `Line`'s meet is our circle center
        lineA.intersectsAt(lineB).map { center =>
          BoundingCircle(center, center.distanceTo(vtxA))
        }

      case _ =>
        None

  /** Creates a `BoundingCircle` from three vertices such that all of the vertices lie on the circles circumference.
    */
  def circumcircle(a: Vertex, b: Vertex, c: Vertex): Option[BoundingCircle] =
    fromThreeVertices(a, b, c)

  /** Creates a `BoundingCircle` that contains all of the points provided.
    */
  def fromVertices(vertices: Batch[Vertex]): BoundingCircle =
    val bb = BoundingBox.fromVertices(vertices)
    BoundingCircle(bb.center, bb.center.distanceTo(bb.topLeft))

  @deprecated("Please use `fromVertices` which is functionally the same as `fromVertexCloud`.")
  def fromVertexCloud(vertices: Batch[Vertex]): BoundingCircle =
    fromVertices(vertices)

  def fromCircle(circle: Circle): BoundingCircle =
    BoundingCircle(
      Vertex.fromPoint(circle.position),
      circle.radius.toDouble
    )

  @deprecated("Please use `BoundingCircle.incircle`, or alternatively `BoundingCircle.circumcircle`.")
  def fromBoundingBox(boundingBox: BoundingBox): BoundingCircle =
    incircle(boundingBox)

  /** Creates a `Circle` from a square (BoundingBox's are squared off by the max width/height) where the circle fits
    * inside the square.
    */
  def incircle(bounds: BoundingBox): BoundingCircle =
    BoundingCircle(bounds.center, Math.max(bounds.halfSize.x, bounds.halfSize.y))

  /** Creates a `BoundingCircle` from a square (BoundingBox's are squared off by the max width/height) such that all of
    * the corners lie on the circle's circumference.
    */
  def circumcircle(bounds: BoundingBox): BoundingCircle =
    val b = bounds.toSquare
    BoundingCircle(b.center, b.center.distanceTo(b.topLeft).toInt)

  def expandToInclude(a: BoundingCircle, b: BoundingCircle): BoundingCircle =
    a.resize(a.position.distanceTo(b.position) + Math.abs(b.radius))

  def encompassing(a: BoundingCircle, b: BoundingCircle): Boolean =
    a.position.distanceTo(b.position) <= Math.abs(a.radius) - Math.abs(b.radius)
  def encompassing(a: BoundingCircle, b: BoundingBox): Boolean =
    b.corners.forall(a.contains)

  def overlapping(a: BoundingCircle, b: BoundingCircle): Boolean =
    a.position.distanceTo(b.position) < Math.abs(a.radius) + Math.abs(b.radius)

  def overlapping(a: BoundingCircle, b: BoundingBox): Boolean =
    b.contains(a.position) || Math.abs(b.sdf(a.position)) <= Math.abs(a.radius)

  def lineIntersects(boundingCircle: BoundingCircle, line: LineSegment): Boolean =
    Math.abs(line.sdf(boundingCircle.position)) <= boundingCircle.radius

  def lineIntersectsAt(boundingCircle: BoundingCircle, line: LineSegment): BoundingCircleLineIntersect =
    val aX = line.start.x
    val aY = line.start.y
    val bX = line.end.x
    val bY = line.end.y
    val dX = bX - aX
    val dY = bY - aY

    if dX == 0 && dY == 0 then BoundingCircleLineIntersect.Zero
    else
      val cX       = boundingCircle.position.x
      val cY       = boundingCircle.position.y
      val dl       = dX * dX + dY * dY
      val t        = ((cX - aX) * dX + (cY - aY) * dY) / dl
      val nearestX = aX + t * dX;
      val nearestY = aY + t * dY;
      val dist     = Vector2.distance(Vector2(nearestX, nearestY), Vector2(cX, cY))
      val r        = boundingCircle.radius

      if dist == r then
        val iX = nearestX;
        val iY = nearestY;

        if t < 0 || t > 1 then BoundingCircleLineIntersect.Zero
        else BoundingCircleLineIntersect.One(Vertex(iX, iY))
      else if dist < r then
        val dt  = Math.sqrt(r * r - dist * dist) / Math.sqrt(dl);
        val t1  = t - dt
        val i1X = aX + t1 * dX
        val i1Y = aY + t1 * dY

        val near =
          if t1 < 0 || t1 > 1 then BoundingCircleLineIntersect.Zero
          else BoundingCircleLineIntersect.One(Vertex(i1X, i1Y))

        val t2  = t + dt
        val i2X = aX + t2 * dX
        val i2Y = aY + t2 * dY

        val far =
          if t2 < 0 || t2 > 1 then BoundingCircleLineIntersect.Zero
          else BoundingCircleLineIntersect.One(Vertex(i2X, i2Y))

        near |+| far
      else BoundingCircleLineIntersect.Zero

  // Centered at the origin
  def signedDistanceFunction(point: Vertex, radius: Double): Double =
    point.length - radius

sealed trait BoundingCircleLineIntersect derives CanEqual:
  def nearest: Option[Vertex] =
    this match
      case BoundingCircleLineIntersect.Zero         => None
      case BoundingCircleLineIntersect.One(at)      => Some(at)
      case BoundingCircleLineIntersect.Two(near, _) => Some(near)

  def furthest: Option[Vertex] =
    this match
      case BoundingCircleLineIntersect.Zero        => None
      case BoundingCircleLineIntersect.One(at)     => Some(at)
      case BoundingCircleLineIntersect.Two(_, far) => Some(far)

  def toOption: Option[List[Vertex]] =
    this match
      case BoundingCircleLineIntersect.Zero           => None
      case BoundingCircleLineIntersect.One(at)        => Some(List(at))
      case BoundingCircleLineIntersect.Two(near, far) => Some(List(near, far))

  def |+|(other: BoundingCircleLineIntersect): BoundingCircleLineIntersect

object BoundingCircleLineIntersect:
  case object Zero extends BoundingCircleLineIntersect:
    def |+|(other: BoundingCircleLineIntersect): BoundingCircleLineIntersect =
      other

  final case class One(at: Vertex) extends BoundingCircleLineIntersect:
    def |+|(other: BoundingCircleLineIntersect): BoundingCircleLineIntersect =
      other match {
        case Zero         => this
        case One(otherAt) => Two(at, otherAt)
        case _: Two       => other
      }

  final case class Two(near: Vertex, far: Vertex) extends BoundingCircleLineIntersect:
    def |+|(other: BoundingCircleLineIntersect): BoundingCircleLineIntersect =
      this




© 2015 - 2024 Weber Informatics LLC | Privacy Policy