'use strict';
const Rac = require('../Rac');
const utils = require('../util/utils');
/**
* Segment of a `[Ray]{@link Rac.Ray}` up to a given length.
*
* @alias Rac.Segment
*/
class Segment {
/**
* Creates a new `Segment` instance.
* @param {Rac} rac - Instance to use for drawing and creating other objects
* @param {Rac.Ray} ray - A `Ray` the segment will be based of
* @param {number} length - The length of the segment
*/
constructor(rac, ray, length) {
// TODO: different approach to error throwing?
// assert || throw new Error(err.missingParameters)
// or
// checker(msg => { throw Rac.Exception.failedAssert(msg));
// .exists(rac)
// .isType(Rac.Ray, ray)
// .isNumber(length)
utils.assertExists(rac, ray, length);
utils.assertType(Rac.Ray, ray);
utils.assertNumber(length);
/**
* Instance of `Rac` used for drawing and passed along to any created
* object.
*
* @type {Rac}
*/
this.rac = rac;
/**
* The `Ray` the segment is based of.
* @type {Rac.Ray}
*/
this.ray = ray;
/**
* The length of the segment.
* @type {number}
*/
this.length = length;
}
/**
* 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.ray.start.x, digits);
const yStr = utils.cutDigits(this.ray.start.y, digits);
const turnStr = utils.cutDigits(this.ray.angle.turn, digits);
const lengthStr = utils.cutDigits(this.length, digits);
return `Segment((${xStr},${yStr}) a:${turnStr} l:${lengthStr})`;
}
/**
* Returns `true` when `ray` and `length` in both segments are equal;
* otherwise returns `false`.
*
* When `otherSegment` is any class other that `Rac.Segment`, returns `false`.
*
* Segments' `length` are compared using `{@link Rac#equals}`.
*
* @param {Rac.Segment} otherSegment - A `Segment` to compare
* @returns {boolean}
* @see Rac.Ray#equals
* @see Rac#equals
*/
equals(otherSegment) {
return otherSegment instanceof Segment
&& this.ray.equals(otherSegment.ray)
&& this.rac.equals(this.length, otherSegment.length);
}
/**
* Returns the `[angle]{@link Rac.Ray#angle}` of the segment's `ray`.
* @returns {Rac.Angle}
*/
angle() {
return this.ray.angle;
}
/**
* Returns the `[start]{@link Rac.Ray#start}` of the segment's `ray`.
* @returns {Rac.Point}
*/
startPoint() {
return this.ray.start;
}
/**
* Returns a new `Point` where the segment ends.
* @returns {Rac.Point}
*/
endPoint() {
return this.ray.pointAtDistance(this.length);
}
/**
* Returns a new `Segment` with angle set to `newAngle`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|number} newAngle - The angle for the new `Segment`
* @returns {Rac.Segment}
*/
withAngle(newAngle) {
newAngle = Rac.Angle.from(this.rac, newAngle);
const newRay = new Rac.Ray(this.rac, this.ray.start, newAngle);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with `ray` set to `newRay`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Ray} newRay - The ray for the new `Segment`
* @returns {Rac.Segment}
*/
withRay(newRay) {
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with start point set to `newStart`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Point} newStartPoint - The start point for the new
* `Segment`
* @returns {Rac.Segment}
*/
withStartPoint(newStartPoint) {
const newRay = this.ray.withStart(newStartPoint);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with `length` set to `newLength`.
*
* All other properties are copied from `this`.
*
* @param {number} newLength - The length for the new `Segment`
* @returns {Rac.Segment}
*/
withLength(newLength) {
return new Segment(this.rac, this.ray, newLength);
}
/**
* Returns a new `Segment` with `length` added to `this.length`.
*
* All other properties are copied from `this`.
*
* @param {number} length - The length to add
* @returns {Rac.Segment}
*/
withLengthAdd(length) {
return new Segment(this.rac, this.ray, this.length + length);
}
/**
* Returns a new `Segment` with `length` set to `this.length * ratio`.
*
* All other properties are copied from `this`.
*
* @param {number} ratio - The factor to multiply `length` by
* @returns {Rac.Segment}
*/
withLengthRatio(ratio) {
return new Segment(this.rac, this.ray, this.length * ratio);
}
/**
* Returns a new `Segment` with `angle` added to `this.angle()`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|number} angle - The angle to add
* @returns {Rac.Segment}
*/
withAngleAdd(angle) {
const newRay = this.ray.withAngleAdd(angle);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with `angle` set to
* `this.ray.{@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.Segment}
*/
withAngleShift(angle, clockwise = true) {
const newRay = this.ray.withAngleShift(angle, clockwise);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with the start point moved in the inverse
* direction of the segment's ray by the given `distance`. The resulting
* `Segment` will have the same `endPoint()` and `angle()` as `this`.
*
* Using a positive `distance` results in a longer segment, using a
* negative `distance` results in a shorter one.
*
* @param {number} distance - The distance to move the start point by
* @returns {Rac.Segment}
*/
withStartExtension(distance) {
const newRay = this.ray.translateToDistance(-distance);
return new Segment(this.rac, newRay, this.length + distance);
}
/**
* Returns a new `Segment` with `distance` added to `this.length`, which
* results in `endPoint()` for the resulting `Segment` moving in the
* direction of the segment's ray by the given `distance`.
*
* All other properties are copied from `this`.
*
* Using a positive `distance` results in a longer segment, using a
* negative `distance` results in a shorter one.
*
* This method performs the same operation as
* `[withLengthAdd]{@link Rac.Segment#withLengthAdd}`.
*
* @param {number} distance - The distance to add to `length`
* @returns {Rac.Segment}
*/
withEndExtension(distance) {
return this.withLengthAdd(distance);
}
/**
* Returns a new `Segment` poiting towards the
* [inverse angle]{@link Rac.Angle#inverse} of `this.angle()`.
*
* The resulting `Segment` will have the same `startPoint()` and `length`
* as `this`.
*
* @returns {Rac.Segment}
* @see Rac.Angle#inverse
*/
inverse() {
const newRay = this.ray.inverse();
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` pointing towards the
* [perpendicular angle]{@link Rac.Angle#perpendicular} of
* `this.angle()` in the `clockwise` orientation.
*
* The resulting `Segment` will have the same `startPoint()` and `length`
* as `this`.
*
* @param {boolean} [clockwise=true] - The orientation of the perpendicular
* @returns {Rac.Segment}
* @see Rac.Angle#perpendicular
*/
perpendicular(clockwise = true) {
const newRay = this.ray.perpendicular(clockwise);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with its start point set at
* `[this.endPoint()]{@link Rac.Segment#endPoint}`,
* angle set to `this.angle().[inverse()]{@link Rac.Angle#inverse}`, and
* same length as `this`.
*
* @returns {Rac.Segment}
* @see Rac.Angle#inverse
*/
reverse() {
const end = this.endPoint();
const inverseRay = new Rac.Ray(this.rac, end, this.ray.angle.inverse());
return new Segment(this.rac, inverseRay, this.length);
}
/**
* Returns a new `Segment` with the start point moved towards `angle` by
* the given `distance`. All other properties are copied from `this`.
*
* @param {Rac.Angle|number} angle - An `Angle` to move the start point
towards
* @param {number} distance - The distance to move the start point by
* @returns {Rac.Segment}
*/
translateToAngle(angle, distance) {
const newRay = this.ray.translateToAngle(angle, distance);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with the start point moved along the segment's
* ray by the given `length`. All other properties are copied from `this`.
*
* When `length` is negative, `start` is moved in the inverse direction of
* `angle`.
*
* @param {number} length - The length to move the start point by
* @returns {Rac.Segment}
*/
translateToLength(length) {
const newRay = this.ray.translateToDistance(length);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns a new `Segment` with the start point moved the given `distance`
* towards the perpendicular angle to `this.angle()` in the `clockwise`
* orientaton. All other properties are copied from `this`.
*
* @param {number} distance - The distance to move the start point by
* @param {boolean} [clockwise=true] - The orientation of the perpendicular
* @returns {Rac.Segment}
*/
translatePerpendicular(distance, clockwise = true) {
const newRay = this.ray.translatePerpendicular(distance, clockwise);
return new Segment(this.rac, newRay, this.length);
}
/**
* Returns the given `value` clamped to [startInset, length-endInset].
*
* When `startInset` is greater that `length-endInset` the range for the
* clamp becomes imposible to fulfill. In this case the returned value
* will be the centered between the range limits and still clampled to
* `[0, length]`.
*
* @param {number} value - A value to clamp
* @param {number} [startInset=0] - The inset for the lower limit of the
* clamping range
* @param {endInset} [endInset=0] - The inset for the higher limit of the
* clamping range
* @returns {number}
*/
clampToLength(value, startInset = 0, endInset = 0) {
const endLimit = this.length - endInset;
if (startInset >= endLimit) {
// imposible range, return middle point
const rangeMiddle = (startInset - endLimit) / 2;
const middle = startInset - rangeMiddle;
// Still clamp to the segment itself
let clamped = middle;
clamped = Math.min(clamped, this.length);
clamped = Math.max(clamped, 0);
return clamped;
}
let clamped = value;
clamped = Math.min(clamped, this.length - endInset);
clamped = Math.max(clamped, startInset);
return clamped;
}
/**
* Returns a new `Point` in the segment's ray at the given `length` from
* `this.startPoint()`. When `length` is negative, the new `Point` is
* calculated in the inverse direction of `this.angle()`.
*
* @param {number} length - The distance from `this.startPoint()`
* @returns {Rac.Point}
* @see Rac.Ray#pointAtDistance
*/
pointAtLength(length) {
return this.ray.pointAtDistance(length);
}
/**
* Returns a new `Point` in the segment's ray at a distance of
* `this.length * ratio` from `this.startPoint()`. When `ratio` is
* negative, the new `Point` is calculated in the inverse direction of
* `this.angle()`.
*
* @param {number} ratio - The factor to multiply `length` by
* @returns {Rac.Point}
* @see Rac.Ray#pointAtDistance
*/
pointAtLengthRatio(ratio) {
return this.ray.pointAtDistance(this.length * ratio);
}
/**
* Returns a new `Point` at the middle point the segment.
* @returns {Rac.Point}
*/
pointAtBisector() {
return this.ray.pointAtDistance(this.length/2);
}
/**
* Returns a new `Segment` starting at `newStartPoint` and ending at
* `this.endPoint()`.
*
* When `newStartPoint` and `this.endPoint()` are considered
* [equal]{@link Rac.Point#equals}, the new `Segment` will use
* `this.angle()`.
*
* @param {Rac.Point} newStartPoint - The start point of the new `Segment`
* @returns {Rac.Segment}
* @see Rac.Point#equals
*/
moveStartPoint(newStartPoint) {
const endPoint = this.endPoint();
return newStartPoint.segmentToPoint(endPoint, this.ray.angle);
}
/**
* Returns a new `Segment` starting at `this.startPoint()` and ending at
* `newEndPoint`.
*
* When `this.startPoint()` and `newEndPoint` are considered
* [equal]{@link Rac.Point#equals}, the new `Segment` will use
* `this.angle()`.
*
* @param {Rac.Point} newEndPoint - The end point of the new `Segment`
* @returns {Rac.Segment}
* @see Rac.Point#equals
*/
moveEndPoint(newEndPoint) {
return this.ray.segmentToPoint(newEndPoint);
}
/**
* Returns a new `Segment` from the starting point to the segment's middle
* point.
*
* @returns {Rac.Segment}
* @see Rac.Segment#pointAtBisector
*/
segmentToBisector() {
return new Segment(this.rac, this.ray, this.length/2);
}
/**
* Returns a new `Segment` from the segment's middle point towards the
* perpendicular angle in the `clockwise` orientation.
*
* The new `Segment` will have the given `length`, or when ommited or
* `null` will use `this.length` instead.
*
* @param {?number} [length=null] - The length of the new `Segment`, or
* `null` to use `this.length`
* @param {boolean} [clockwise=true] - The orientation of the perpendicular
* @returns {Rac.Segment}
* @see Rac.Segment#pointAtBisector
* @see Rac.Angle#perpendicular
*/
segmentBisector(length = null, clockwise = true) {
const newStart = this.pointAtBisector();
const newAngle = this.ray.angle.perpendicular(clockwise);
const newRay = new Rac.Ray(this.rac, newStart, newAngle);
const newLength = length === null
? this.length
: length;
return new Segment(this.rac, newRay, newLength);
}
/**
* Returns a new `Segment` starting from `endPoint()` with the given
* `length` and the same angle as `this`.
*
* @param {number} length - The length of the next `Segment`
* @returns {Rac.Segment}
*/
nextSegmentWithLength(length) {
const newStart = this.endPoint();
const newRay = this.ray.withStart(newStart);
return new Segment(this.rac, newRay, length);
}
/**
* Returns a new `Segment` starting from `endPoint()` and up to the given
* `nextEndPoint`.
*
* When `endPoint()` and `nextEndPoint` are considered
* [equal]{@link Rac.Point#equals}, the new `Segment` will use
* `this.angle()`.
*
* @param {Rac.Point} nextEndPoint - The end point of the next `Segment`
* @returns {Rac.Segment}
* @see Rac.Point#equals
*/
nextSegmentToPoint(nextEndPoint) {
const newStart = this.endPoint();
return newStart.segmentToPoint(nextEndPoint, this.ray.angle);
}
/**
* Returns a new `Segment` starting from `endPoint()` towards `angle`
* with the given `length`.
*
* The new `Segment` will have the given `length`, or when ommited or
* `null` will use `this.length` instead.
*
* @param {Rac.Angle|number} angle - The angle of the new `Segment`
* @param {?number} [length=null] - The length of the new `Segment`, or
* `null` to use `this.length`
* @returns {Rac.Segment}
*/
nextSegmentToAngle(angle, length = null) {
angle = Rac.Angle.from(this.rac, angle);
const newLength = length === null
? this.length
: length;
const newStart = this.endPoint();
const newRay = new Rac.Ray(this.rac, newStart, angle);
return new Segment(this.rac, newRay, newLength);
}
/**
* Returns a new `Segment` starting from `endPoint()` towards the given
* `angleDistance` from `this.angle().inverse()` in the `clockwise`
* orientation.
*
* The new `Segment` will have the given `length`, or when ommited or
* `null` will use `this.length` instead.
*
* Notice that the `angleDistance` is applied to the inverse of the
* segment's angle. E.g. with an `angleDistance` of `0` the resulting
* `Segment` will be directly over and pointing in the inverse angle of
* `this`. As the `angleDistance` increases the two segments separate with
* the pivot at `endPoint()`.
*
* @param {Rac.Angle|number} angleDistance - An angle distance to apply to
* the segment's angle inverse
* @param {boolean} [clockwise=true] - The orientation of the angle shift
* from `endPoint()`
* @param {?number} [length=null] - The length of the new `Segment`, or
* `null` to use `this.length`
* @returns {Rac.Segment}
* @see Rac.Angle#inverse
*/
nextSegmentToAngleDistance(angleDistance, clockwise = true, length = null) {
angleDistance = this.rac.Angle.from(angleDistance);
const newLength = length === null ? this.length : length;
const newRay = this.ray
.translateToDistance(this.length)
.inverse()
.withAngleShift(angleDistance, clockwise);
return new Segment(this.rac, newRay, newLength);
}
/**
* Returns a new `Segment` starting from `endPoint()` towards the
* `[perpendicular angle]{@link Rac.Angle#perpendicular}` of
* `this.angle().inverse()` in the `clockwise` orientation.
*
* The new `Segment` will have the given `length`, or when ommited or
* `null` will use `this.length` instead.
*
* Notice that the perpendicular is calculated from the inverse of the
* segment's angle. E.g. with `clockwise` as `true`, the resulting
* `Segment` will be pointing towards `this.angle().perpendicular(false)`.
*
* @param {boolean} [clockwise=true] - The orientation of the
* perpendicular angle from `endPoint()`
* @param {?number} [length=null] - The length of the new `Segment`, or
* `null` to use `this.length`
* @returns {Rac.Segment}
* @see Rac.Angle#perpendicular
*/
nextSegmentPerpendicular(clockwise = true, length = null) {
const newLength = length === null
? this.length
: length;
const newRay = this.ray
.translateToDistance(this.length)
.perpendicular(!clockwise);
return new Segment(this.rac, newRay, newLength);
}
/**
* Returns a new `Segment` starting from `endPoint()` which corresponds
* to the leg of a right triangle where `this` is the other cathetus and
* the hypotenuse is of length `hypotenuse`.
*
* The new `Segment` will point towards the perpendicular angle of
* `[this.angle().[inverse()]{@link Rac.Angle#inverse}` in the `clockwise`
* orientation.
*
* When `hypotenuse` is smaller that the segment's `length`, returns
* `null` since no right triangle is possible.
*
* @param {number} hypotenuse - The length of the hypotenuse side of the
* right triangle formed with `this` and the new `Segment`
* @param {boolean} [clockwise=true] - The orientation of the
* perpendicular angle from `endPoint()`
* @returns {Rac.Segment}
* @see Rac.Angle#inverse
*/
nextSegmentLegWithHyp(hypotenuse, clockwise = true) {
if (hypotenuse < this.length) {
return null;
}
// cos = ady / hyp
const radians = Math.acos(this.length / hypotenuse);
// tan = ops / adj
// tan * adj = ops
const ops = Math.tan(radians) * this.length;
return this.nextSegmentPerpendicular(clockwise, ops);
}
/**
* Returns a new `Arc` based on this segment, with the given `endAngle`
* and `clockwise` orientation.
*
* The returned `Arc` will use this segment's start as `center`, its angle
* as `start`, and its length as `radius`.
*
* When `endAngle` is ommited or `null`, the segment's angle is used
* instead resulting in a complete-circle arc.
*
* @param {?Rac.Angle} [endAngle=null] - An `Angle` to use as end for the
* new `Arc`, or `null` to use `this.angle()`
* @param {boolean} [clockwise=true] - The orientation of the new `Arc`
* @returns {Rac.Arc}
*/
arc(endAngle = null, clockwise = true) {
endAngle = endAngle === null
? this.ray.angle
: Rac.Angle.from(this.rac, endAngle);
return new Rac.Arc(this.rac,
this.ray.start, this.length,
this.ray.angle, endAngle,
clockwise);
}
/**
* Returns a new `Arc` based on this segment, with the arc's end at
* `angleDistance` from the segment's angle in the `clockwise`
* orientation.
*
* The returned `Arc` will use this segment's start as `center`, its angle
* as `start`, and its length as `radius`.
*
* @param {Rac.Angle|number} angleDistance - The angle distance from the
* segment's start to the new `Arc` end
* @param {boolean} [clockwise=true] - The orientation of the new `Arc`
* @returns {Rac.Arc}
*/
arcWithAngleDistance(angleDistance, clockwise = true) {
angleDistance = this.rac.Angle.from(angleDistance);
const stargAngle = this.ray.angle;
const endAngle = stargAngle.shift(angleDistance, clockwise);
return new Rac.Arc(this.rac,
this.ray.start, this.length,
stargAngle, endAngle,
clockwise);
}
// TODO: uncomment once beziers are tested again
// bezierCentralAnchor(distance, clockwise = true) {
// let bisector = this.segmentBisector(distance, clockwise);
// return new Rac.Bezier(this.rac,
// this.start, bisector.end,
// bisector.end, this.end);
// }
} // Segment
module.exports = Segment;