'use strict';
const Rac = require('../Rac');
const utils = require('../util/utils');
/**
* Point in a two dimentional coordinate system.
*
* Several methods will return an adjusted value or perform adjustments in
* their operation when two points are close enough as to be considered
* equal. When the the difference of each coordinate of two points
* is under the `[equalityThreshold]{@link Rac#equalityThreshold}` the
* points are considered equal. The `[equals]{@link Rac.Point#equals}`
* method performs this check.
*
* @alias Rac.Point
*/
class Point{
/**
* Creates a new `Point` instance.
* @param {Rac} rac - Instance to use for drawing and creating other objects
* @param {number} x - The x coordinate
* @param {number} y - The y coordinate
*/
constructor(rac, x, y) {
utils.assertExists(rac, x, y);
utils.assertNumber(x, y);
/**
* Intance of `Rac` used for drawing and passed along to any created
* object.
* @type {Rac}
*/
this.rac = rac;
/**
* X coordinate of the point.
* @type {number}
*/
this.x = x;
/**
* Y coordinate of the point.
* @type {number}
*/
this.y = y;
}
/**
* Returns a string representation intended for human consumption.
*
* @param {number} [digits] - The number of digits to print after the
* decimal point, when ommited all digits are printed
* @returns {string}
*/
toString(digits = null) {
const xStr = utils.cutDigits(this.x, digits);
const yStr = utils.cutDigits(this.y, digits);
return `Point(${xStr},${yStr})`;
}
/**
* Returns `true` when the difference with `otherPoint` for each coordinate is
* under `{@link Rac#equalityThreshold}`, otherwise returns `false`.
*
* When `otherPoint` is any class other that `Rac.Point`, returns `false`.
*
* Values are compared using `{@link Rac#equals}`.
*
* @param {Rac.Point} otherPoint - A `Point` to compare
* @returns {boolean}
* @see Rac#equals
*/
equals(otherPoint) {
return otherPoint instanceof Point
&& this.rac.equals(this.x, otherPoint.x)
&& this.rac.equals(this.y, otherPoint.y);
}
/**
* Returns a new `Point` with `x` set to `newX`.
* @param {number} newX - The x coordinate for the new `Point`
* @returns {Rac.Point}
*/
withX(newX) {
return new Point(this.rac, newX, this.y);
}
/**
* Returns a new `Point` with `x` set to `newX`.
* @param {number} newY - The y coordinate for the new `Point`
* @returns {Rac.Point}
*/
withY(newY) {
return new Point(this.rac, this.x, newY);
}
/**
* Returns a new `Point` with `x` added to `this.x`.
* @param {number} x - The x coordinate to add
* @returns {Rac.Point}
*/
addX(x) {
return new Point(this.rac,
this.x + x, this.y);
}
/**
* Returns a new `Point` with `y` added to `this.y`.
* @param {number} y - The y coordinate to add
* @returns {Rac.Point}
*/
addY(y) {
return new Point(this.rac,
this.x, this.y + y);
}
/**
* Returns a new `Point` by adding the components of `point` to `this`.
* @param {Rac.Point} point - A `Point` to add
* @returns {Rac.Point}
*/
addPoint(point) {
return new Point(
this.rac,
this.x + point.x,
this.y + point.y);
}
/**
* Returns a new `Point` by adding the `x` and `y` components to `this`.
* @param {number} x - The x coodinate to add
* @param {number} y - The y coodinate to add
* @returns {Rac.Point}
*/
add(x, y) {
return new Point(this.rac,
this.x + x, this.y + y);
}
/**
* Returns a new `Point` by subtracting the components of `point`.
* @param {Rac.Point} point - A `Point` to subtract
* @returns {Rac.Point}
*/
subtractPoint(point) {
return new Point(
this.rac,
this.x - point.x,
this.y - point.y);
}
/**
* Returns a new `Point` by subtracting the `x` and `y` components.
* @param {number} x - The x coodinate to subtract
* @param {number} y - The y coodinate to subtract
* @returns {Rac.Point}
*/
subtract(x, y) {
return new Point(
this.rac,
this.x - x,
this.y - y);
}
/**
* Returns a new `Point` with the negative coordinate values of `this`.
* @returns {Rac.Point}
*/
negative() {
return new Point(this.rac, -this.x, -this.y);
}
/**
* Returns the distance from `this` to `point`, or returns `0` when
* `this` and `point` are considered [equal]{@link Rac.Point#equals}.
*
* @param {Rac.Point} point - A `Point` to measure the distance to
* @returns {number}
* @see Rac.Point#equals
*/
distanceToPoint(point) {
if (this.equals(point)) {
return 0;
}
const x = Math.pow((point.x - this.x), 2);
const y = Math.pow((point.y - this.y), 2);
return Math.sqrt(x+y);
}
/**
* Returns the angle from `this` to `point`.
*
* When `this` and `point` are considered
* [equal]{@link Rac.Point#equals}, returns the angle produced with
* `defaultAngle`.
*
* @param {Rac.Point} point - A `Point` to measure the angle to
* @param {Rac.Angle|number} [defaultAngle=instance.Angle.Zero] -
* An `Angle` to return when `this` and `point` are equal
* @returns {Rac.Angle}
* @see Rac.Point#equals
*/
angleToPoint(point, defaultAngle = this.rac.Angle.zero) {
if (this.equals(point)) {
defaultAngle = this.rac.Angle.from(defaultAngle);
return defaultAngle;
}
const offset = point.subtractPoint(this);
const radians = Math.atan2(offset.y, offset.x);
return Rac.Angle.fromRadians(this.rac, radians);
}
/**
* Returns a new `Point` at a `distance` from `this` in the direction of
* `angle`.
*
* @param {Rac.Angle|number} angle - An `Angle` towars the new `Point`
* @param {number} distance - The distance to the new `Point`
* @returns {Rac.Point}
*/
pointToAngle(angle, distance) {
angle = this.rac.Angle.from(angle);
const distanceX = distance * Math.cos(angle.radians());
const distanceY = distance * Math.sin(angle.radians());
return new Point(this.rac, this.x + distanceX, this.y + distanceY);
}
/**
* Returns a new `Ray` from `this` towards `angle`.
* @param {Rac.Angle|number} angle - The `Angle` of the new `Ray`
* @returns {Rac.Ray}
*/
ray(angle) {
angle = this.rac.Angle.from(angle);
return new Rac.Ray(this.rac, this, angle);
}
/**
* Returns a new `Ray` from `this` towards `point`.
*
* When `this` and `point` are considered equal, the new `Ray` will use
* the angle produced with `defaultAngle`.
*
* @param {Rac.Point} point - A `Point` to point the `Ray` towards
* @param {Rac.Angle|number} [defaultAngle=instance.Angle.Zero] -
* An `Angle` to use when `this` and `point` are equal
* @returns {Rac.Ray}
*/
rayToPoint(point, defaultAngle = this.rac.Angle.zero) {
defaultAngle = this.angleToPoint(point, defaultAngle);
return new Rac.Ray(this.rac, this, defaultAngle);
}
/**
* Returns a new `Ray` from `this` to the projection of `this` in `ray`.
*
* When the projected point is equal to `this` the produced ray will have
* an angle perpendicular to `ray` in the clockwise direction.
*
* @param {Rac.Ray} ray - A `Ray` to project `this` onto
* @returns {Rac.Ray}
*/
rayToProjectionInRay(ray) {
const projected = ray.pointProjection(this);
const perpendicular = ray.angle.perpendicular();
return this.rayToPoint(projected, perpendicular);
}
/**
* @summary
* Returns a new `Ray` that starts at `this` and is tangent to `arc`, when
* no tangent is possible returns `null`.
*
* @description
* The new `Ray` will be in the `clockwise` side of the ray formed
* from `this` towards `arc.center`. `arc` is considered a complete
* circle.
*
* When `this` is inside `arc` no tangent segment is possible and `null`
* is returned.
*
* A special case is considered when `arc.radius` is considered to be `0`
* and `this` is equal to `arc.center`. In this case the angle between
* `this` and `arc.center` is assumed to be the inverse of `arc.start`,
* thus the new `Ray` will have an angle perpendicular to
* `arc.start.inverse()`, in the `clockwise` orientation.
*
* @param {Rac.Arc} arc - An `Arc` to calculate a tangent to, considered
* as a complete circle
* @param {boolean} [clockwise=true] - the orientation of the new `Ray`
* @return {Rac.Ray?}
*/
rayTangentToArc(arc, clockwise = true) {
// A default angle is given for the edge case of a zero-radius arc
let hypotenuse = this.segmentToPoint(arc.center, arc.start.inverse());
let ops = arc.radius;
if (this.rac.equals(hypotenuse.length, arc.radius)) {
// Point in arc
const perpendicular = hypotenuse.ray.angle.perpendicular(clockwise);
return new Rac.Ray(this.rac, this, perpendicular);
}
if (this.rac.equals(hypotenuse.length, 0)) {
return null;
}
let angleSine = ops / hypotenuse.length;
if (angleSine > 1) {
// Point inside arc
return null;
}
let angleRadians = Math.asin(angleSine);
let opsAngle = Rac.Angle.fromRadians(this.rac, angleRadians);
let shiftedOpsAngle = hypotenuse.angle().shift(opsAngle, clockwise);
return new Rac.Ray(this.rac, this, shiftedOpsAngle);
}
/**
* Returns a new `Segment` from `this` towards `angle` with the given
* `length`.
*
* @param {Rac.Angle|number} angle - An `Angle` to point the segment
* towards
* @param {number} length - The length of the new `Segment`
* @returns {Rac.Segment}
*/
segmentToAngle(angle, length) {
angle = this.rac.Angle.from(angle);
const ray = new Rac.Ray(this.rac, this, angle);
return new Rac.Segment(this.rac, ray, length);
}
/**
* Returns a new `Segment` from `this` to `point`.
*
* When `this` and `point` are considered [equal]{@link Rac.Point#equals},
* the new `Segment` will use the angle produced with `defaultAngle`.
*
* @param {Rac.Point} point - A `Point` to point the `Segment` towards
* @param {Rac.Angle|number} [defaultAngle=instance.Angle.Zero] -
* An `Angle` to use when `this` and `point` are equal
* @returns {Rac.Segment}
* @see Rac.Point#equals
*/
segmentToPoint(point, defaultAngle = this.rac.Angle.zero) {
defaultAngle = this.angleToPoint(point, defaultAngle);
const length = this.distanceToPoint(point);
const ray = new Rac.Ray(this.rac, this, defaultAngle);
return new Rac.Segment(this.rac, ray, length);
}
/**
* Returns a new `Segment` from `this` to the projection of `this` in
* `ray`.
*
* When the projected point is equal to `this`, the new `Segment` will
* have an angle perpendicular to `ray` in the clockwise direction.
*
* @param {Rac.Ray} ray - A `Ray` to project `this` onto
* @returns {Rac.Segment}
*/
segmentToProjectionInRay(ray) {
const projected = ray.pointProjection(this);
const perpendicular = ray.angle.perpendicular();
return this.segmentToPoint(projected, perpendicular);
}
/**
* @summary
* Returns a new `Segment` that starts at `this` and is tangent to `arc`,
* when no tangent is possible returns `null`.
*
* @description
* The new `Segment` will be in the `clockwise` side of the ray formed
* from `this` towards `arc.center`, and its end point will be at the
* contact point with `arc` which is considered as a complete circle.
*
* When `this` is inside `arc` no tangent segment is possible and `null`
* is returned.
*
* A special case is considered when `arc.radius` is considered to be `0`
* and `this` is equal to `arc.center`. In this case the angle between
* `this` and `arc.center` is assumed to be the inverse of `arc.start`,
* thus the new `Segment` will have an angle perpendicular to
* `arc.start.inverse()`, in the `clockwise` orientation.
*
* @param {Rac.Arc} arc - An `Arc` to calculate a tangent to, considered
* as a complete circle
* @param {boolean} [clockwise=true] - the orientation of the new `Segment`
* @return {Rac.Segment?}
*/
segmentTangentToArc(arc, clockwise = true) {
const tangentRay = this.rayTangentToArc(arc, clockwise);
if (tangentRay === null) {
return null;
}
const tangentPerp = tangentRay.angle.perpendicular(clockwise);
const radiusRay = arc.center.ray(tangentPerp);
return tangentRay.segmentToIntersection(radiusRay);
}
/**
* Returns a new `Arc` with center at `this` and the given arc properties.
*
* @param {number} radius - The radius of the new `Arc`
* @param {Rac.Angle|number} [someStart=rac.Angle.zero] - The start
* `Angle` of the new `Arc`
* @param {?Rac.Angle|number} [someEnd=null] - The end `Angle` of the new
* `Arc`; when `null` or ommited, `start` is used instead
* @param {boolean=} clockwise=true - The orientation of the new `Arc`
* @returns {Rac.Arc}
*/
arc(
radius,
start = this.rac.Angle.zero,
end = null,
clockwise = true)
{
start = this.rac.Angle.from(start);
end = end === null
? start
: this.rac.Angle.from(end);
return new Rac.Arc(this.rac, this, radius, start, end, clockwise);
}
/**
* Returns a new `Text` with the given `string` and `format`.
* @param {string} string - The string of the new `Text`
* @param {Rac.Text.Format} format - The format of the new `Text`
* @returns {Rac.Text}
*/
text(string, format) {
return new Rac.Text(this.rac, this, string, format);
}
} // class Point
module.exports = Point;