shared/RoundedLine.js

'use strict';

/**
 * A simple line hitbox. Remember that this line describes the item as if
 * its settings were x=0, y=0, scale=1, rotation=0.
 *
 * The hitbox supports a line whose enpoint is rounded with a radius of width/2.
 *
 * @memberof module:shared
 * @implements {module:shared.Hitbox}
 *
 * @param {number} [x1=0] - The x1 offset of the line.
 * @param {number} [y1=0] - The y1 offset of the line.
 * @param {number} [x2=1] - The x2 offset of the line.
 * @param {number} [y2=1] - The y2 offset of the line.
 * @param {number} [width=1] - The width of the line.
 */
class RoundedLine {
  constructor(x1 = 0, y1 = 0, x2 = 1, y2 = 1, width = 1) {
    /**
     * The x1 coordinate of the line.
     *
     * @type {number}
     * @default 0
     */
    this.x1 = x1;

    /**
     * The y1 coordinate of the line.
     *
     * @type {number}
     * @default 0
     */
    this.y1 = y1;

    /**
     * The x2 coordinate of the line.
     *
     * @type {number}
     * @default 1
     */
    this.x2 = x2;

    /**
     * The y1 coordinate of the line.
     *
     * @type {number}
     * @default 1
     */
    this.y2 = y2;

    /**
     * The width of the line.
     *
     * @type {number}
     * @default 1
     */
    this.width = width;
  }

  /**
   * Determines if a point is inside the line.
   *
   * @see {@link https://stackoverflow.com/a/6853926/20044396}
   * @see {@link https://web.archive.org/web/20210507021429/https://geomalgorithms.com/a02-_lines.html}
   *
   * @param {module:shared.Point2D} point - The point to test.
   *
   * @return {boolean} true if the point is inside the line, false otherwise.
   */
  contains(point) {
    // This is blatantly ripped from the stack overflow response provided above
    // with comments and renamed variables to try to clarify.

    // Distance to first endpoint
    const dx1 = point.x - this.x1;
    const dy1 = point.y - this.y1;

    // Length of line components
    const lengthX = this.x2 - this.x1;
    const lengthY = this.y2 - this.y1;

    // Find the "projection distance" - the fraction along the line from (x1, y1) towards (x2, y2)
    // that is closest to the point
    const dot = dx1 * lengthX + dy1 * lengthY;
    const lengthSquared = lengthX * lengthX + lengthY * lengthY;
    let projectionDistance = -1;
    if (lengthSquared !== 0)
      // in case of 0-length line
      projectionDistance = dot / lengthSquared;

    // Find the nearest point along the line
    let xx, yy;
    if (projectionDistance < 0) {
      xx = this.x1;
      yy = this.y1;
    } else if (projectionDistance > 1) {
      xx = this.x2;
      yy = this.y2;
    } else {
      xx = this.x1 + projectionDistance * lengthX;
      yy = this.y1 + projectionDistance * lengthY;
    }

    // Is the distance to the nearest point less than half the width?
    const dx = point.x - xx;
    const dy = point.y - yy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    return dist <= this.width / 2;
  }
}

module.exports = RoundedLine;