Source: coordinate.mjs

/**
 * Class representing a single 2D point (x and y).
 *
 * @property {number} x
 * @property {number} y
 */
class Coordinate {
	x;
	y;

	/**
	 * Generate a coordninate from x and y numbers.
	 *
	 * @param {number} x
	 * @param {number} y
	 */
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}

	/**
	 * Compares the position of this to an given coordinate.
	 *
	 * @param {Coordinate} other - the coordinate to compare against
	 * @returns {boolean} - true if coordinates have the same position
	 */
	equals(other) {
		return !!other && this.x === other.x && this.y === other.y;
	}

	/**
	 * Serializes the coordinate for TikZ.
	 *
	 * @example new Coordinate(1, 2).serializeName();
	 * // returns (1, 2)
	 *
	 * @returns {String} the serialized coordinate
	 */
	serializeName() {
		return "(" + this.x + ", " + this.y + ")";
	}

	/**
	 * Alias for `serializeName()`.
	 * @see {@link serializeName} for details
	 *
	 * @returns {String} the serialized coordinate
	 */
	toString() {
		return this.serializeName();
	}

	/**
	 * Calculates the distance of two points.
	 *
	 * @param {Coordinate} other - the other coordinate
	 * @returns {number} the distance
	 */
	getDistance(other) {
		return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
	}

	/**
	 * Add other coordinate (x and y) to this coordinate.
	 *
	 * @example
	 * const a = new Coordinate(2,2);
	 * const b = new Coordinate(1,1);
	 * a.add(b);	// a += b
	 * // a is now (3,3)
	 *
	 * @param {Coordinate} other the coordinate to add
	 * @returns {Coordinate} this coordinate
	 */
	add(other) {
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	/**
	 * Subtract other coordinate (x and y) from this coordinate.
	 *
	 * @example
	 * const a = new Coordinate(2,2);
	 * const b = new Coordinate(1,1);
	 * a.subtract(b);	// a -= b
	 * // a is now (1,1)
	 *
	 * @param {Coordinate} other - the coordinate to subtract
	 * @returns {Coordinate} `this`
	 */
	subtract(other) {
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}

	/**
	 * Calculates the sum of this and other given coordinates. This coordinate will not be altered, instead a new
	 * coordinate will bw returned.
	 *
	 * @param  {...Coordinate} others - other coordinates to sum up
	 * @returns {Coordinate} the new Coordinate (sum)
	 */
	sum(...others) {
		const coordSum = new Coordinate(this.x, this.y);
		others.forEach(coordSum.add);
		return coordSum;
	}

	/**
	 * Rotate the Coordinate around `centerCoord`. The rotation is counter clockwise, like the default mathematical
	 * rotation.
	 *
	 * @param {number} angle - rotation angle in degrees
	 * @param {Coordinate} [centerCoord] - center of rotation
	 * @returns {Coordinate} `this`
	 */
	rotate(angle, centerCoord) {
		// normalize angle --> -180 < angle <= 180
		while (angle <= -180) angle += 360;
		while (angle > 180) angle -= 360;

		if (!!centerCoord) this.subtract(centerCoord);

		switch (angle) {
			case 0:
				// case 360:
				break;

			// case -270:
			case 90: {
				const oldX = this.x;
				this.x = -this.y;
				this.y = oldX;
				break;
			}

			// case -180:
			case 180:
				this.x = -this.x;
				this.y = -this.y;
				break;

			case -90: {
				//case 270:
				const oldX = this.x;
				this.x = this.y;
				this.y = -oldX;
				break;
			}

			default: {
				const oldX = this.x;
				const oldY = this.y;
				const radians = (Math.PI / 180) * angle,
					cos = Math.cos(radians),
					sin = Math.sin(radians);

				this.x = cos * oldX - sin * oldY;
				this.y = sin * oldX + cos * oldY;
				break;
			}
		}

		if (!!centerCoord) this.add(centerCoord);

		return this;
	}

	/**
	 * Projects a point perpendicular on a line.
	 *
	 * @param {Coordinate} lineStart - first coord of the line
	 * @param {Coordinate} lineEnd - second coord of the line
	 * @returns {Coordinate} the Coordinate on the line (new instance)
	 */
	orthogonalProjection(lineStart, lineEnd) {
		const lineVector = lineEnd.clone().subtract(lineStart); // lineEnd - lineStart
		const helpVector = this.clone().subtract(lineStart); // this - lineStart

		const lambda =
			(lineVector.x * helpVector.x + lineVector.y * helpVector.y) / (lineVector.x ** 2 + lineVector.y ** 2);

		return lineVector.scale(lambda, true).add(lineStart);
	}

	/**
	 * Scale the coordinate with one or two factors.
	 *
	 * @param {number} [scaleX=1] - factor for the x coordinate
	 * @param {number|true} [scaleY=1] - factor for the y coordinate or true to use scaleX
	 * @returns {Coordinate} `this`
	 */
	scale(scaleX = 1, scaleY = 1) {
		if (Number.isFinite(scaleX)) this.x *= scaleX;
		if (Number.isFinite(scaleY)) this.y *= scaleY;
		else if (Number.isFinite(scaleX) && scaleY === true) this.y *= scaleX;
		return this;
	}

	/**
	 * Creates a new instance with the same coordinates.
	 *
	 * @returns {Coordinate} a new instance of this coordinate
	 */
	clone() {
		return new Coordinate(this.x, this.y);
	}

	/**
	 * Mirrors the coordinate on the x axis (inverts y).
	 *
	 * @returns {Coordinate} `this`
	 */
	mirrorX() {
		this.y = -this.y;
		return this;
	}

	/**
	 * Mirrors the coordinate on the y axis (inverts x).
	 *
	 * @returns {Coordinate} `this`
	 */
	mirrorY() {
		this.x = -this.x;
		return this;
	}
}

export { Coordinate };