'use strict';
const Rac = require('../Rac');
const utils = require('../util/utils');
/**
* Unbounded ray from a `[Point]{@link Rac.Point}` in direction of an
* `[Angle]{@link Rac.Angle}`.
*
* ### `instance.Ray`
*
* Instances of `Rac` contain a convenience
* [`rac.Ray` function]{@link Rac#Ray} to create `Ray` objects from
* primitive values. This function also contains ready-made convenience
* objects, like [`rac.Ray.xAxis`]{@link instance.Ray#xAxis}, listed under
* [`instance.Ray`]{@link instance.Ray}.
*
* @example
* let rac = new Rac()
* let point = rac.Point(55, 77)
* let angle = rac.Angle(1/4)
* // new instance with constructor
* let ray = new Rac.Ray(rac, point, angle)
* // or convenience function
* let otherRay = rac.Ray(55, 77, 1/4)
*
* @see [`rac.Ray`]{@link Rac#Ray}
* @see [`instance.Ray`]{@link instance.Ray}
*
* @alias Rac.Ray
*/
class Ray {
/**
* Creates a new `Ray` instance.
* @param {Rac} rac Instance to use for drawing and creating other objects
* @param {Rac.Point} start - A `Point` where the ray starts
* @param {Rac.Angle} angle - An `Angle` the ray is directed to
*/
constructor(rac, start, angle) {
utils.assertExists(rac, start, angle);
utils.assertType(Rac.Point, start);
utils.assertType(Rac.Angle, angle);
/**
* Instance of `Rac` used for drawing and passed along to any created
* object.
*
* @type {Rac}
*/
this.rac = rac;
/**
* The start point of the ray.
* @type {Rac.Point}
*/
this.start = start;
/**
* The angle towards which the ray extends.
* @type {Rac.Angle}
*/
this.angle = angle;
}
/**
* 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.start.x, digits);
const yStr = utils.cutDigits(this.start.y, digits);
const turnStr = utils.cutDigits(this.angle.turn, digits);
return `Ray((${xStr},${yStr}) a:${turnStr})`;
}
/**
* Returns `true` when `start` and `angle` in both rays are equal;
* otherwise returns `false`.
*
* When `otherRay` is any class other that `Rac.Ray`, returns `false`.
*
* @param {Rac.Ray} otherRay - A `Ray` to compare
* @returns {Boolean}
* @see Rac.Point#equals
* @see Rac.Angle#equals
*/
equals(otherRay) {
return otherRay instanceof Ray
&& this.start.equals(otherRay.start)
&& this.angle.equals(otherRay.angle);
}
/**
* Returns the slope of the ray, or `null` if the ray is vertical.
*
* In the line formula `y = mx + b` the slope is `m`.
*
* @returns {?Number}
*/
slope() {
let isVertical =
this.rac.unitaryEquals(this.angle.turn, this.rac.Angle.down.turn)
|| this.rac.unitaryEquals(this.angle.turn, this.rac.Angle.up.turn);
if (isVertical) {
return null;
}
return Math.tan(this.angle.radians());
}
/**
* Returns the y-intercept: the point at which the ray, extended in both
* directions, intercepts with the y-axis; or `null` if the ray is
* vertical.
*
* In the line formula `y = mx + b` the y-intercept is `b`.
*
* @returns {?Number}
*/
yIntercept() {
let slope = this.slope();
if (slope === null) {
return null;
}
// y = mx + b
// y - mx = b
return this.start.y - slope * this.start.x;
}
/**
* Returns a new `Ray` with `start` set to `newStart`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Point} newStart - The start for the new `Ray`
* @returns {Rac.Ray}
*/
withStart(newStart) {
return new Ray(this.rac, newStart, this.angle);
}
/**
* Returns a new `Ray` with `start.x` set to `newX`.
*
* All other properties are copied from `this`.
*
* @param {Number} newX - The x coordinate for the new `Ray`
* @returns {Rac.Ray}
*/
withX(newX) {
return new Ray(this.rac, this.start.withX(newX), this.angle);
}
/**
* Returns a new `Ray` with `start.y` set to `newY`.
*
* All other properties are copied from `this`.
*
* @param {Number} newY - The y coordinate for the new `Ray`
* @returns {Rac.Ray}
*/
withY(newY) {
return new Ray(this.rac, this.start.withY(newY), this.angle);
}
/**
* Returns a new `Ray` with `angle` set to `newAngle`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|Number} newAngle - The angle for the new `Ray`
* @returns {Rac.Ray}
*/
withAngle(newAngle) {
newAngle = this.rac.Angle.from(newAngle);
return new Ray(this.rac, this.start, newAngle);
}
/**
* Returns a new `Ray` with `angle` added to `this.angle`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|Number} angle - The angle to add
* @returns {Rac.Ray}
*/
withAngleAdd(angle) {
let newAngle = this.angle.add(angle);
return new Ray(this.rac, this.start, newAngle);
}
/**
* Returns a new `Ray` with `angle` set to
* `this.{@link Rac.Angle#shift angle.shift}(angle, clockwise)`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|Number} angle - The angle to be shifted by
* @param {Boolean} [clockwise=true] - The orientation of the shift
* @returns {Rac.Ray}
*/
withAngleShift(angle, clockwise = true) {
let newAngle = this.angle.shift(angle, clockwise);
return new Ray(this.rac, this.start, newAngle);
}
/**
* Returns a new `Ray` pointing towards
* `{@link Rac.Angle#inverse angle.inverse()}`.
* @returns {Rac.Ray}
*/
inverse() {
const inverseAngle = this.angle.inverse();
return new Ray(this.rac, this.start, inverseAngle);
}
/**
* Returns a new `Ray` pointing towards the
* [perpendicular angle]{@link Rac.Angle#perpendicular} of
* `angle` in the `clockwise` orientation.
*
* @param {Boolean} [clockwise=true] - The orientation of the perpendicular
* @returns {Rac.Ray}
* @see Rac.Angle#perpendicular
*/
perpendicular(clockwise = true) {
let perpendicular = this.angle.perpendicular(clockwise);
return new Ray(this.rac, this.start, perpendicular);
}
/**
* Returns a new `Ray` with `start` moved along the ray by the given
* `distance`. All other properties are copied from `this`.
*
* When `distance` is negative, `start` is moved in
* the inverse direction of `angle`.
*
* @param {Number} distance - The distance to move `start` by
* @returns {Rac.Ray}
*/
translateToDistance(distance) {
const newStart = this.start.pointToAngle(this.angle, distance);
return new Ray(this.rac, newStart, this.angle);
}
/**
* Returns a new `Ray` with `start` moved towards `angle` by the given
* `distance`. All other properties are copied from `this`.
*
* @param {Rac.Angle|Number} angle - An `Angle` to move `start` towards
* @param {Number} distance - The distance to move `start` by
* @returns {Rac.Ray}
*/
translateToAngle(angle, distance) {
const newStart = this.start.pointToAngle(angle, distance);
return new Ray(this.rac, newStart, this.angle);
}
/**
* Returns a new `Ray` with `start` moved by the given distance toward the
* `angle.perpendicular()`. All other properties are copied from `this`.
*
* @param {Number} distance - The distance to move `start` by
* @param {Boolean} [clockwise=true] - The orientation of the perpendicular
* @returns {Rac.Ray}
*/
translatePerpendicular(distance, clockwise = true) {
let perpendicular = this.angle.perpendicular(clockwise);
return this.translateToAngle(perpendicular, distance);
}
/**
* Returns the angle from `this.start` to `point`.
*
* When `this.start` and `point` are considered
* [equal]{@link Rac.Point#equals}, returns `this.angle`.
*
* @param {Rac.Point} point - A `Point` to measure the angle to
* @returns {Rac.Angle}
* @see Rac.Point#equals
*/
angleToPoint(point) {
return this.start.angleToPoint(point, this.angle);
}
/**
* Returns a new `Point` located in the ray where the x coordinate is `x`.
* When the ray is vertical, returns `null` since no single point with x
* coordinate at `x` is possible.
*
* The ray is considered a unbounded line.
*
* @param {Number} x - The x coordinate to calculate a point in the ray
* @returns {Rac.Point}
*/
pointAtX(x) {
const slope = this.slope();
if (slope === null) {
// Vertical ray
return null;
}
if (this.rac.unitaryEquals(slope, 0)) {
// Horizontal ray
return this.start.withX(x);
}
// y = mx + b
const y = slope * x + this.yIntercept();
return new Rac.Point(this.rac, x, y);
}
/**
* Returns a new `Point` located in the ray where the y coordinate is `y`.
* When the ray is horizontal, returns `null` since no single point with y
* coordinate at `y` is possible.
*
* The ray is considered an unbounded line.
*
* @param {Number} y - The y coordinate to calculate a point in the ray
* @returns {Rac.Point}
*/
pointAtY(y) {
const slope = this.slope();
if (slope === null) {
// Vertical ray
return this.start.withY(y);
}
if (this.rac.unitaryEquals(slope, 0)) {
// Horizontal ray
return null;
}
// mx + b = y
// x = (y - b)/m
const x = (y - this.yIntercept()) / slope;
return new Rac.Point(this.rac, x, y);
}
/**
* Returns a new `Point` in the ray at the given `distance` from
* `this.start`. When `distance` is negative, the new `Point` is calculated
* in the inverse direction of `angle`.
*
* @param {Number} distance - The distance from `this.start`
* @returns {Rac.Point}
*/
pointAtDistance(distance) {
return this.start.pointToAngle(this.angle, distance);
}
/**
* Returns a new `Point` at the intersection of `this` and `otherRay`.
*
* When the rays are parallel, returns `null` since no intersection is
* possible.
*
* Both rays are considered unbounded lines.
*
* @param {Rac.Ray} otherRay - A `Ray` to calculate the intersection with
* @returns {Rac.Point}
*/
pointAtIntersection(otherRay) {
const a = this.slope();
const b = otherRay.slope();
// Parallel lines, no intersection
if (a === null && b === null) { return null; }
if (this.rac.unitaryEquals(a, b)) { return null; }
// Any vertical ray
if (a === null) { return otherRay.pointAtX(this.start.x); }
if (b === null) { return this.pointAtX(otherRay.start.x); }
const c = this.yIntercept();
const d = otherRay.yIntercept();
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
const x = (d - c) / (a - b);
const y = a * x + c;
return new Rac.Point(this.rac, x, y);
}
/**
* Returns a new `Point` at the projection of `point` onto the ray. The
* projected point is the closest possible point to `point`.
*
* The ray is considered an unbounded line.
*
* @param {Rac.Point} point - A `Point` to project onto the ray
* @returns {Rac.Point}
*/
pointProjection(point) {
const perpendicular = this.angle.perpendicular();
return point.ray(perpendicular)
.pointAtIntersection(this);
}
/**
* Returns the distance from `this.start` to the projection of `point`
* onto the ray.
*
* The returned distance is positive when the projected point is towards
* the direction of the ray, and negative when it is behind.
*
* @param {Rac.Point} point - A `Point` to project and measure the
* distance to
* @returns {Number}
*/
distanceToProjectedPoint(point) {
const projected = this.pointProjection(point);
const distance = this.start.distanceToPoint(projected);
if (this.rac.equals(distance, 0)) {
return 0;
}
const angleToProjected = this.start.angleToPoint(projected);
const angleDiff = this.angle.subtract(angleToProjected);
if (angleDiff.turn <= 1/4 || angleDiff.turn > 3/4) {
return distance;
} else {
return -distance;
}
}
/**
* Returns `true` when the angle to the given `point` is located clockwise
* of the ray or `false` when located counter-clockwise.
*
* * When `this.start` and `point` are considered
* [equal]{@link Rac.Point#equals} or `point` lands on the ray, it is
* considered clockwise. When `point` lands on the
* [inverse]{@link Rac.Ray#inverse} of the ray, it is considered
* counter-clockwise.
*
* @param {Rac.Point} point - A `Point` to measure the orientation to
* @returns {Boolean}
*
* @see Rac.Point#equals
* @see Rac.Ray#inverse
*/
pointOrientation(point) {
const pointAngle = this.start.angleToPoint(point, this.angle);
if (this.angle.equals(pointAngle)) {
return true;
}
const angleDistance = pointAngle.subtract(this.angle);
// [0 to 0.5) is considered clockwise
// [0.5, 1) is considered counter-clockwise
return angleDistance.turn < 0.5;
}
/**
* Returns a new `Ray` from `this.start` to `point`.
*
* When `this.start` and `point` are considered
* [equal]{@link Rac.Point#equals}, the new `Ray` will use `this.angle`.
*
* @param {Rac.Point} point - A `Point` to point the `Ray` towards
* @returns {Rac.Ray}
* @see Rac.Point#equals
*/
rayToPoint(point) {
let newAngle = this.start.angleToPoint(point, this.angle);
return new Ray(this.rac, this.start, newAngle);
}
/**
* Returns a new `Segment` using `this` and the given `length`.
* @param {Number} length - The length of the new `Segment`
* @returns {Rac.Segment}
*/
segment(length) {
return new Rac.Segment(this.rac, this, length);
}
/**
* Returns a new `Segment` from `this.start` to `point`.
*
* When `this.start` and `point` are considered
* [equal]{@link Rac.Point#equals}, the new `Segment` will use
* `this.angle`.
*
* @param {Rac.Point} point - A `Point` to point the `Segment` towards
* @returns {Rac.Segment}
* @see Rac.Point#equals
*/
segmentToPoint(point) {
return this.start.segmentToPoint(point, this.angle);
}
/**
* Returns a new `Segment` starting at `this.start` and ending at the
* intersection of `this` and `otherRay`.
*
* When the rays are parallel, returns `null` since no intersection is
* possible.
*
* When `this.start` and the intersection point are considered
* [equal]{@link Rac.Point#equals}, the new `Segment` will use
* `this.angle`.
*
* Both rays are considered unbounded lines.
*
* @param {Rac.Ray} otherRay - A `Ray` to calculate the intersection with
* @returns {Rac.Segment}
*/
segmentToIntersection(otherRay) {
const intersection = this.pointAtIntersection(otherRay);
if (intersection === null) {
return null;
}
return this.segmentToPoint(intersection);
}
/**
* Returns a new `Arc` with center at `this.start`, start at `this.angle`
* and the given arc properties.
*
* @param {Number} radius - The radius of the new `Arc`
* @param {?Rac.Angle|Number} [endAngle=null] - The end `Angle` of the new
* `Arc`; when `null` or ommited, `this.angle` is used instead
* @param {Boolean} [clockwise=true] - The orientation of the new `Arc`
* @returns {Rac.Arc}
*/
arc(radius, endAngle = null, clockwise = true) {
endAngle = endAngle === null
? this.angle
: this.rac.Angle.from(endAngle);
return new Rac.Arc(this.rac,
this.start, radius,
this.angle, endAngle,
clockwise);
}
/**
* Returns a new `Arc` with center at `this.start`, start at `this.angle`,
* and end at the given `angleDistance` from `this.start` in the
* `clockwise` orientation.
*
* @param {Number} radius - The radius of the new `Arc`
* @param {Rac.Angle|Number} angleDistance - The angle distance from
* `this.start` to the new `Arc` end
* @param {Boolean} [clockwise=true] - The orientation of the new `Arc`
* @returns {Rac.Arc}
*/
arcToAngleDistance(radius, angleDistance, clockwise = true) {
let endAngle = this.angle.shift(angleDistance, clockwise);
return new Rac.Arc(this.rac,
this.start, radius,
this.angle, endAngle,
clockwise);
}
// TODO: Leaving undocumented for now, until better use/explanation is found
// based on https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
bezierArc(otherRay) {
if (this.start.equals(otherRay.start)) {
// When both rays have the same start, returns a point bezier.
return new Rac.Bezier(this.rac,
this.start, this.start,
this.start, this.start);
}
let intersection = this.perpendicular()
.pointAtIntersection(otherRay.perpendicular());
let orientation = null;
let radiusA = null;
let radiusB = null;
// Check for parallel rays
if (intersection !== null) {
// Normal intersection case
orientation = this.pointOrientation(intersection);
radiusA = intersection.segmentToPoint(this.start);
radiusB = intersection.segmentToPoint(otherRay.start);
} else {
// In case of parallel rays, otherRay gets shifted to be
// perpendicular to this.
let shiftedIntersection = this.perpendicular()
.pointAtIntersection(otherRay);
if (shiftedIntersection === null || this.start.equals(shiftedIntersection)) {
// When both rays lay on top of each other, the shifting produces
// rays with the same start; function returns a linear bezier.
return new Rac.Bezier(this.rac,
this.start, this.start,
otherRay.start, otherRay.start);
}
intersection = this.start.pointAtBisector(shiftedIntersection);
// Case for shifted intersection between two rays
orientation = this.pointOrientation(intersection);
radiusA = intersection.segmentToPoint(this.start);
radiusB = radiusA.inverse();
}
const angleDistance = radiusA.angle().distance(radiusB.angle(), orientation);
const quarterAngle = angleDistance.mult(1/4);
// TODO: what happens with square angles? is this covered by intersection logic?
const quarterTan = quarterAngle.tan();
const tangentA = quarterTan * radiusA.length * 4/3;
const anchorA = this.pointAtDistance(tangentA);
const tangentB = quarterTan * radiusB.length * 4/3;
const anchorB = otherRay.pointAtDistance(tangentB);
return new Rac.Bezier(this.rac,
this.start, anchorA,
anchorB, otherRay.start);
}
} // class Ray
module.exports = Ray;