/******************************************************************************** * Copyright (c) 2017-2021 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { hasOwnProperty } from "./object"; /** * A Point is composed of the (x,y) coordinates of an object. */ export interface Point { readonly x: number readonly y: number } export namespace Point { /** * (x,y) coordinates of the origin. */ export const ORIGIN: Point = Object.freeze({ x: 0, y: 0 }); /** * Adds two points. * @param {Point} p1 - First point * @param {Point} p2 - Second point * @returns {Point} The sum of the two points */ export function add(p1: Point, p2: Point): Point { return { x: p1.x + p2.x, y: p1.y + p2.y }; } /** * Subtracts two points. * @param {Point} p1 - First point * @param {Point} p2 - Second point * @returns {Point} The difference of the two points */ export function subtract(p1: Point, p2: Point): Point { return { x: p1.x - p2.x, y: p1.y - p2.y }; } /** * Specifies whether a point has exactly the same coordinates as another point. * @param {Point} point1 a point * @param {Point} point2 another point * @returns {boolean} `true` if `point1` has exactly the same `x` and `y` values as `point2`, `false` otherwise. */ export function equals(point1: Point, point2: Point): boolean { return point1.x === point2.x && point1.y === point2.y; } /** * Computes a point that is the original `point` shifted towards `refPoint` by the given `distance`. * @param {Point} point - Point to shift * @param {Point} refPoint - Point to shift towards * @param {Point} distance - Distance to shift */ export function shiftTowards(point: Point, refPoint: Point, distance: number): Point { const diff = subtract(refPoint, point); const normalized = normalize(diff); const shift = { x: normalized.x * distance, y: normalized.y * distance }; return add(point, shift); } /** * Computes the normalized vector from the vector given in `point`; that is, computing its unit vector. * @param {Point} point - Point representing the vector to be normalized * @returns {Point} The normalized point */ export function normalize(point: Point): Point { const mag = magnitude(point); if (mag === 0 || mag === 1) { return ORIGIN; } return { x: point.x / mag, y: point.y / mag }; } /** * Computes the magnitude of the vector given in `point`. * @param {Point} point - Point representing the vector to compute the magnitude for * @returns {number} The magnitude or also known as length of the `point` */ export function magnitude(point: Point): number { return Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2)); } /** * Calculates a linear combination of p0 and p1 using lambda, i.e. * (1-lambda) * p0 + lambda * p1 */ export function linear(p0: Point, p1: Point, lambda: number): Point { return { x: (1 - lambda) * p0.x + lambda * p1.x, y: (1 - lambda) * p0.y + lambda * p1.y }; } /** * Returns the "straight line" distance between two points. * @param {Point} a - First point * @param {Point} b - Second point * @returns {number} The Eucledian distance */ export function euclideanDistance(a: Point, b: Point): number { const dx = b.x - a.x; const dy = b.y - a.y; return Math.sqrt(dx * dx + dy * dy); } /** * Returns the distance between two points in a grid, using a * strictly vertical and/or horizontal path (versus straight line). * @param {Point} a - First point * @param {Point} b - Second point * @returns {number} The Manhattan distance */ export function manhattanDistance(a: Point, b: Point): number { return Math.abs(b.x - a.x) + Math.abs(b.y - a.y); } /** * Returns the maximum of the horizontal and the vertical distance. * @param {Point} a - First point * @param {Point} b - Second point * @returns {number} The maximum distance */ export function maxDistance(a: Point, b: Point): number { return Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y)); } /** * Returns the dot product of two points. * @param {Point} a - First point * @param {Point} b - Second point * @returns {number} The dot product */ export function dotProduct(a: Point, b: Point): number { return a.x * b.x + a.y * b.y; } } /** * Computes the angle in radians of the given point to the x-axis of the coordinate system. * The result is in the range [-pi, pi]. * @param {Point} p - A point in the Eucledian plane */ export function angleOfPoint(p: Point): number { return Math.atan2(p.y, p.x); } /** * Computes the angle in radians between the two given points (relative to the origin of the coordinate system). * The result is in the range [0, pi]. Returns NaN if the points are equal. * @param {Point} a - First point * @param {Point} b - Second point */ export function angleBetweenPoints(a: Point, b: Point): number { const lengthProduct = Math.sqrt((a.x * a.x + a.y * a.y) * (b.x * b.x + b.y * b.y)); if (isNaN(lengthProduct) || lengthProduct === 0) return NaN; const dotProduct = a.x * b.x + a.y * b.y; return Math.acos(dotProduct / lengthProduct); } /** * Computes the center of the line segment spanned by the two given points. * @param {Point} s - Start point of the line * @param {Point} e - End point of the line */ export function centerOfLine(s: Point, e: Point): Point { const b: Bounds = { x: s.x > e.x ? e.x : s.x, y: s.y > e.y ? e.y : s.y, width: Math.abs(e.x - s.x), height: Math.abs(e.y - s.y) }; return Bounds.center(b); } /** * The Dimension of an object is composed of its width and height. */ export interface Dimension { readonly width: number readonly height: number } export namespace Dimension { /** * A dimension with both width and height set to a negative value, which is considered as undefined. */ export const EMPTY: Dimension = Object.freeze({ width: -1, height: -1 }); /** * Checks whether the given dimention is valid, i.e. the width and height are non-zero. * @param {Dimension} b - Dimension object * @returns {boolean} `true` if the dimension is valid */ export function isValid(d: Dimension): boolean { return d.width >= 0 && d.height >= 0; } } /** * The bounds are the position (x, y) and dimension (width, height) of an object. */ export interface Bounds extends Point, Dimension { } export function isBounds(element: unknown): element is Bounds { return hasOwnProperty(element, ['x', 'y', 'width', 'height']); } export namespace Bounds { export const EMPTY: Bounds = Object.freeze({ x: 0, y: 0, width: -1, height: -1 }); /** * Combines the bounds of two objects into one, so that the new bounds * are the minimum bounds that covers both of the original bounds. * @param {Bounds} b0 - First bounds object * @param {Bounds} b1 - Second bounds object * @returns {Bounds} The combined bounds */ export function combine(b0: Bounds, b1: Bounds): Bounds { if (!Dimension.isValid(b0)) return Dimension.isValid(b1) ? b1 : EMPTY; if (!Dimension.isValid(b1)) return b0; const minX = Math.min(b0.x, b1.x); const minY = Math.min(b0.y, b1.y); const maxX = Math.max(b0.x + (b0.width >= 0 ? b0.width : 0), b1.x + (b1.width >= 0 ? b1.width : 0)); const maxY = Math.max(b0.y + (b0.height >= 0 ? b0.height : 0), b1.y + (b1.height >= 0 ? b1.height : 0)); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } /** * Translates the given bounds. * @param {Bounds} b - Bounds object * @param {Point} p - Vector by which to translate the bounds * @returns {Bounds} The translated bounds */ export function translate(b: Bounds, p: Point): Bounds { return { x: b.x + p.x, y: b.y + p.y, width: b.width, height: b.height }; } /** * Returns the center point of the bounds of an object * @param {Bounds} b - Bounds object * @returns {Point} the center point */ export function center(b: Bounds): Point { return { x: b.x + (b.width >= 0 ? 0.5 * b.width : 0), y: b.y + (b.height >= 0 ? 0.5 * b.height : 0) }; } /** * Checks whether the point p is included in the bounds b. */ export function includes(b: Bounds, p: Point): boolean { return p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height; } } /** * Converts from radians to degrees * @param {number} a - A value in radians * @returns {number} The converted value */ export function toDegrees(a: number): number { return a * 180 / Math.PI; } /** * Converts from degrees to radians * @param {number} a - A value in degrees * @returns {number} The converted value */ export function toRadians(a: number): number { return a * Math.PI / 180; } /** * Returns whether two numbers are almost equal, within a small margin (0.001) * @param {number} a - First number * @param {number} b - Second number * @returns {boolean} True if the two numbers are almost equal */ export function almostEquals(a: number, b: number): boolean { return Math.abs(a - b) < 1e-3; }