chromium/ash/webui/camera_app_ui/resources/js/geometry.ts

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from './assert.js';

/**
 * Size of Rectangle.
 */
export class Size {
  constructor(readonly width: number, readonly height: number) {}
}

/**
 * 2D point.
 */
export class Point {
  constructor(readonly x: number, readonly y: number) {}
}

const ORIGIN = new Point(0, 0);

/**
 * 2D vector.
 */
export class Vector {
  constructor(readonly x: number, readonly y: number) {}

  length(): number {
    return Math.hypot(this.x, this.y);
  }

  /**
   * Square distance.
   */
  length2(): number {
    return this.x * this.x + this.y * this.y;
  }

  add(v: Vector): Vector {
    return new Vector(this.x + v.x, this.y + v.y);
  }

  minus(v: Vector): Vector {
    return new Vector(this.x - v.x, this.y - v.y);
  }

  /**
   * Dot product.
   */
  dot(v: Vector): number {
    return this.x * v.x + this.y * v.y;
  }

  /**
   * Cross product.
   */
  cross(v: Vector): number {
    return this.x * v.y - this.y * v.x;
  }

  multiply(n: number): Vector {
    return new Vector(this.x * n, this.y * n);
  }

  /**
   * @return Angle required to rotate from this vector to |v|.
   *     Positive/negative sign represent rotating in (counter-)clockwise
   *     direction.
   */
  rotation(v: Vector): number {
    const cross = this.cross(v);
    const dot = this.dot(v);
    return Math.atan2(cross, dot);
  }

  /**
   * The rotation angle for setting |CSSRotate|.
   */
  cssRotateAngle(): number {
    return ROTATE_START_AXIS.rotation(this);
  }

  /**
   * Unit direction vector.
   */
  direction(): Vector {
    const length = this.length();
    return new Vector(this.x / length, this.y / length);
  }

  /**
   * Unit normal vector n in direction that the |this| x |n| is positive.
   */
  normal(): Vector {
    const length = this.length();
    return new Vector(-this.y / length, this.x / length);
  }

  /**
   * @return Returns the vector point to reverse direction.
   */
  reverse(): Vector {
    return new Vector(-this.x, -this.y);
  }

  point(): Point {
    return new Point(this.x, this.y);
  }
}

/**
 * @return Vector points from |start| to |end|.
 */
export function vectorFromPoints(end: Point, start = ORIGIN): Vector {
  return new Vector(end.x - start.x, end.y - start.y);
}

/**
 * Start axis of |CSSRotate|.
 */
const ROTATE_START_AXIS = new Vector(1, 0);

/**
 * Vector with polar representation.
 */
export class PolarVector extends Vector {
  /**
   * @param angle Counter closewise angle start from (1, 0) in rad.
   */
  constructor(angle: number, length: number) {
    super(Math.cos(angle) * length, Math.sin(angle) * length);
  }
}

/**
 * Line formed by a point and a vector.
 */
export class Line {
  constructor(readonly point: Point, readonly direction: Vector) {}

  /**
   * Move parallelly in |this.direction.normal()| direction with specified
   * distance.
   */
  moveParallel(distance: number): Line {
    const newPt = vectorFromPoints(this.point)
                      .add(this.direction.normal().multiply(distance))
                      .point();
    return new Line(newPt, this.direction);
  }

  /**
   * @return Intersection with another line.
   */
  intersect(line: Line): Point|null {
    // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/geometry/float_polygon.cc;drc=977ef9c6935c51f8e4fdb1b0a81fdba23bf5563c;l=214
    const det = this.direction.cross(line.direction);
    if (det === 0) {
      return null;
    }
    const ptDelta = vectorFromPoints(this.point, line.point);
    const u = line.direction.cross(ptDelta) / det;
    return vectorFromPoints(this.point).add(this.direction.multiply(u)).point();
  }

  /**
   * @return If the |pt| fall in the half plane in normal vector
   *     direction divided by this line.
   */
  isInward(pt: Point): boolean {
    const v = vectorFromPoints(pt, this.point);
    return this.direction.cross(v) >= 0;
  }
}

/**
 * Bounding box position at origin.
 */
export class Box {
  constructor(readonly size: Size) {}

  inside(pt: Point): boolean {
    return 0 <= pt.x && pt.x <= this.size.width && 0 <= pt.y &&
        pt.y <= this.size.height;
  }

  /**
   * Calculates intersection with a ray formed by a start point(inside box) and
   * a pointing direction.
   *
   * @param pt Start point of the ray.
   * @param dir Direction of the ray.
   * @return The intersection point.
   */
  rayIntersect(pt: Point, dir: Vector): Point {
    assert(
        0 <= pt.x && pt.x <= this.size.width && 0 <= pt.y &&
        pt.y <= this.size.height);
    assert(dir.x !== 0 || dir.y !== 0);

    if (dir.x === 0) {
      // Horizontal ray should intersect one of the vertical sides.
      return new Point(pt.x, dir.y > 0 ? this.size.height : 0);
    } else {
      // Check the wall hit when moving in x component direction.
      const toWall = (dir.x > 0 ? this.size.width - pt.x : pt.x);
      const hitY = pt.y + dir.y * toWall / Math.abs(dir.x);
      if (0 <= hitY && hitY <= this.size.height) {
        return new Point(dir.x > 0 ? this.size.width : 0, hitY);
      }
    }
    if (dir.y === 0) {
      // Vertical ray should intersect one of the horizontal sides.
      return new Point(dir.x > 0 ? this.size.width : 0, pt.y);
    } else {
      // Check the wall hit when moving in y component direction.
      const toWall = (dir.y > 0 ? this.size.height - pt.y : pt.y);
      const hitX = pt.x + dir.x * toWall / Math.abs(dir.y);
      assert(0 <= hitX && hitX <= this.size.width);
      return new Point(hitX, dir.y > 0 ? this.size.height : 0);
    }
  }

  /**
   * Calculates intersection with segment.
   *
   * @return Intersections with segment formed by |pt| and |pt2|.
   */
  segmentIntersect(pt: Point, pt2: Point): Point[] {
    /**
     * Intersection point of two line segments in 2 dimensions:
     * http://paulbourke.net/geometry/pointlineplane/.
     *
     * @return Intersection of segment pt1, pt2 and segment pt3, pt4.
     *     Returns null for no intersection between two segment.
     */
    function intersect(pt1: Point, pt2: Point, pt3: Point, pt4: Point): Point|
        null {
      const uDenom =
          (pt4.y - pt3.y) * (pt2.x - pt1.x) - (pt4.x - pt3.x) * (pt2.y - pt1.y);
      if (uDenom === 0) {
        return null;
      }
      const ua = ((pt4.x - pt3.x) * (pt1.y - pt3.y) -
                  (pt4.y - pt3.y) * (pt1.x - pt3.x)) /
          uDenom;
      const ub = ((pt2.x - pt1.x) * (pt1.y - pt3.y) -
                  (pt2.y - pt1.y) * (pt1.x - pt3.x)) /
          uDenom;
      if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
        return new Point(
            pt1.x + ua * (pt2.x - pt1.x), pt1.y + ua * (pt2.y - pt1.y));
      }
      return null;
    }

    const cornRD = new Point(this.size.width, this.size.height);
    const cornLD = new Point(0, this.size.height);
    const cornLU = new Point(0, 0);
    const cornRU = new Point(this.size.width, 0);
    const segs: Array<[Point, Point]> = [
      [cornRU, cornLU],
      [cornRD, cornRU],
      [cornLU, cornLD],
      [cornLD, cornRD],
    ];

    const intersectPts = [];
    for (const seg of segs) {
      const intersectPt = intersect(pt, pt2, seg[0], seg[1]);
      if (intersectPt !== null) {
        intersectPts.push(intersectPt);
      }
    }

    return intersectPts;
  }
}