raycaster/raycaster.js

import { Body2D, Shape } from "../physics/index.js"
import { Ray } from "./ray.js"
import { Vector2 } from "../math/index.js"
import { RayCollisionResult, RayPoint } from "./raycastresult.js"

export class Raycast2D {
  /**
   * @template  T
   * @param {Ray} ray
   * @param {Body2D} body
   * @param {T} value
   * @returns {RayCollisionResult<T>}
   */
  static castToBody(ray, body, value) {
    return Raycast2D.cast(ray,body.shapes[0],value)
  }
  /**
   * @template {Shape} T
   * @template  U
   * @param {Ray} ray
   * @param {T} shape
   * @param {U} value
   * @param {RayCollisionResult<U>} results
   */
  static cast(ray, shape, value, results = new RayCollisionResult(value)) {
    if (shape.type === Shape.POLYGON)
      return Raycast2D.testVertices(ray, shape.vertices, results)
    if (shape.type === Shape.CIRCLE)
      return Raycast2D.testCircle(ray, shape.vertices[0], shape.vertices[1].x, results)
    return results
  }
  /**
   * @template T
   * @param {Ray} ray
   * @param {Vector_like} position
   * @param {number} radius
   * @param {RayCollisionResult<T>} results 
   */
  static testCircle(ray, position, radius, results) {
    const x1 = ray.origin.x
    const y1 = ray.origin.y
    const x2 = ray.direction.x
    const y2 = ray.direction.y

    const x3 = position.x
    const y3 = position.y
    const x4 = x3 - x1
    const y4 = y3 - y1
    const r = radius

    const proj = x2 * x4 + y2 * y4
    const delta = proj * proj - ((x4 * x4 + y4 * y4) - r * r)
    const sqrtDelta = Math.sqrt(delta)
    const distance1 = proj + sqrtDelta
    const distance2 = proj - sqrtDelta

    if (delta < 0 || distance1 < 0) return results
    results.points.push(new RayPoint(
      new Vector2(
        x1 + distance1 * x2,
        y1 + distance1 * y2
      ), distance1 * distance1
    ))
    if (delta === 0 || (distance2 < 0)) return results
    results.points.push(new RayPoint(
      new Vector2(
        x1 + distance2 * x2,
        y1 + distance2 * y2
      ),
      distance2 * distance2
    ))
    return results
  }
  /**
   * @template T
   * @param {Ray} ray
   * @param {Vector2[]} vertices
   * @param {RayCollisionResult<T>} result 
   */
  static testVertices(ray, vertices, result) {
    const origin = ray.origin
    const direction = ray.direction

    const res = testSingleEdge(
      vertices[vertices.length - 1],
      vertices[0], ray.origin, ray.direction
    )
    if (res) result.points.push(
      new RayPoint(
        res,
        Vector2.magnitudeSquared(Vector2.sub(res,origin))
      )
    )
    for (let i = 0; i < vertices.length - 1; i++) {
      let res = testSingleEdge(
        vertices[i], vertices[i + 1],
        origin, direction
      )
      if (res) result.points.push(
        new RayPoint(
          res,
          Vector2.magnitudeSquared(Vector2.sub(res,origin))
        )
      )
    }
    return result
  }
}

/**
 * @param {Vector2} v1
 * @param {Vector2} v2
 * @param {Vector2} or
 * @param {Vector2} dir
 */
function testSingleEdge(v1, v2, or, dir) {
  const x1 = v1.x
  const y1 = v1.y
  const x2 = v2.x
  const y2 = v2.y
  const x3 = or.x
  const y3 = or.y
  const x4 = dir.x + x3
  const y4 = dir.y + y3
  const den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)

  if (den === 0) return null

  const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / den
  const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / den

  if (
    t > 0 && t < 1 &&
    u > 0
  ) return new Vector2(
    x1 + t * (x2 - x1),
    y1 + t * (y2 - y1)
  )
  return null
}