'use strict';
const Rac = require('../Rac');
const utils = require('../util/utils');
/**
* Arc of a circle from a `start` to an `end` [angle]{@link Rac.Angle}.
*
* Arcs that have [equal]{@link Rac.Angle#equals} `start` and `end` angles
* are considered a complete circle.
*
* @alias Rac.Arc
*/
class Arc{
/**
* Creates a new `Arc` instance.
*
* @param {Rac} rac - Instance to use for drawing and creating other objects
* @param {Rac.Point} center - The center of the arc
* @param {number} radius - The radius of the arc
* @param {Rac.Angle} start - An `Angle` where the arc starts
* @param {Rac.Angle} end - Ang `Angle` where the arc ends
* @param {boolean} clockwise - The orientation of the arc
*/
constructor(rac,
center, radius,
start, end,
clockwise)
{
utils.assertExists(rac, center, radius, start, end, clockwise);
utils.assertType(Rac.Point, center);
utils.assertNumber(radius);
utils.assertType(Rac.Angle, start, end);
utils.assertBoolean(clockwise);
/**
* Instance of `Rac` used for drawing and passed along to any created
* object.
*
* @type {Rac}
*/
this.rac = rac;
/**
* The center `Point` of the arc.
* @type {Rac.Point}
*/
this.center = center;
/**
* The radius of the arc.
* @type {number}
*/
this.radius = radius;
/**
* The start `Angle` of the arc. The arc is draw from this angle towards
* `end` in the `clockwise` orientation.
*
* When `start` and `end` are [equal angles]{@link Rac.Angle#equals}
* the arc is considered a complete circle.
*
* @type {Rac.Angle}
* @see Rac.Angle#equals
*/
this.start = start
/**
* The end `Angle` of the arc. The arc is draw from `start` to this
* angle in the `clockwise` orientation.
*
* When `start` and `end` are [equal angles]{@link Rac.Angle#equals}
* the arc is considered a complete circle.
*
* @type {Rac.Angle}
* @see Rac.Angle#equals
*/
this.end = end;
/**
* The orientiation of the arc.
* @type {boolean}
*/
this.clockwise = clockwise;
}
/**
* 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.center.x, digits);
const yStr = utils.cutDigits(this.center.y, digits);
const radiusStr = utils.cutDigits(this.radius, digits);
const startStr = utils.cutDigits(this.start.turn, digits);
const endStr = utils.cutDigits(this.end.turn, digits);
return `Arc((${xStr},${yStr}) r:${radiusStr} s:${startStr} e:${endStr} c:${this.clockwise}})`;
}
/**
* Returns `true` when all members of both arcs are equal.
*
* When `otherArc` is any class other that `Rac.Arc`, returns `false`.
*
* Arcs' `radius` are compared using `{@link Rac#equals}`.
*
* @param {Rac.Segment} otherSegment - A `Segment` to compare
* @returns {boolean}
* @see Rac.Point#equals
* @see Rac.Angle#equals
* @see Rac#equals
*/
equals(otherArc) {
return otherArc instanceof Arc
&& this.rac.equals(this.radius, otherArc.radius)
&& this.clockwise === otherArc.clockwise
&& this.center.equals(otherArc.center)
&& this.start.equals(otherArc.start)
&& this.end.equals(otherArc.end);
}
/**
* Returns the length of the arc: the part of the circumference the arc
* represents.
* @returns {number}
*/
length() {
return this.angleDistance().turnOne() * this.radius * Rac.TAU;
}
/**
* Returns the length of circumference of the arc considered as a complete
* circle.
* @returns {number}
*/
circumference() {
return this.radius * Rac.TAU;
}
/**
* Returns a new `Angle` that represents the distance between `start` and
* `end`, in the orientation of the arc.
* @returns {Rac.Angle}
*/
angleDistance() {
return this.start.distance(this.end, this.clockwise);
}
/**
* Returns a new `Point` located where the arc starts.
* @returns {Rac.Point}
*/
startPoint() {
return this.pointAtAngle(this.start);
}
/**
* Returns a new `Point` located where the arc ends.
* @returns {Rac.Point}
*/
endPoint() {
return this.pointAtAngle(this.end);
}
/**
* Returns a new `Ray` from `center` towars `start`.
* @returns {Rac.Ray}
*/
startRay() {
return new Rac.Ray(this.rac, this.center, this.start);
}
/**
* Returns a new `Ray` from `center` towars `end`.
* @returns {Rac.Ray}
*/
endRay() {
return new Rac.Ray(this.rac, this.center, this.end);
}
/**
* Returns a new `Segment` from `center` to `startPoint()`.
* @returns {Rac.Segment}
*/
startSegment() {
return new Rac.Segment(this.rac, this.startRay(), this.radius);
}
/**
* Returns a new `Segment` from `center` to `endPoint()`.
* @returns {Rac.Segment}
*/
endSegment() {
return new Rac.Segment(this.rac, this.endRay(), this.radius);
}
/**
* Returns a new `Segment` from `startPoint()` to `endPoint()`.
*
* Note that for complete circle arcs this segment will have a length of
* zero and be pointed towards the perpendicular of `start` in the arc's
* orientation.
*
* @returns {Rac.Segment}
*/
chordSegment() {
const perpendicular = this.start.perpendicular(this.clockwise);
return this.startPoint().segmentToPoint(this.endPoint(), perpendicular);
}
/**
* Returns `true` if the arc is a complete circle, which is when `start`
* and `end` are [equal angles]{@link Rac.Angle#equals}.
*
* @returns {boolean}
* @see Rac.Angle#equals
*/
isCircle() {
return this.start.equals(this.end);
}
/**
* Returns a new `Arc` with center set to `newCenter`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Point} newCenter - The center for the new `Arc`
* @returns {Rac.Arc}
*/
withCenter(newCenter) {
return new Arc(this.rac,
newCenter, this.radius,
this.start, this.end,
this.clockwise);
}
/**
* Returns a new `Arc` with start set to `newStart`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|number} newStart - The start for the new `Arc`
* @returns {Rac.Arc}
*/
withStart(newStart) {
const newStartAngle = Rac.Angle.from(this.rac, newStart);
return new Arc(this.rac,
this.center, this.radius,
newStartAngle, this.end,
this.clockwise);
}
/**
* Returns a new `Arc` with end set to `newEnd`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|number} newEnd - The end for the new `Arc`
* @returns {Rac.Arc}
*/
withEnd(newEnd) {
const newEndAngle = Rac.Angle.from(this.rac, newEnd);
return new Arc(this.rac,
this.center, this.radius,
this.start, newEndAngle,
this.clockwise);
}
/**
* Returns a new `Arc` with radius set to `newRadius`.
*
* All other properties are copied from `this`.
*
* @param {number} newRadius - The radius for the new `Arc`
* @returns {Rac.Arc}
*/
withRadius(newRadius) {
return new Arc(this.rac,
this.center, newRadius,
this.start, this.end,
this.clockwise);
}
/**
* Returns a new `Arc` with its orientation set to `newClockwise`.
*
* All other properties are copied from `this`.
*
* @param {boolean} newClockwise - The orientation for the new `Arc`
* @returns {Rac.Arc}
*/
withClockwise(newClockwise) {
return new Arc(this.rac,
this.center, this.radius,
this.start, this.end,
newClockwise);
}
/**
* Returns a new `Arc` with the given `angleDistance` as the distance
* between `start` and `end` in the arc's orientation. This changes `end`
* for the new `Arc`.
*
* All other properties are copied from `this`.
*
* @param {Rac.Angle|number} angleDistance - The angle distance of the
* new `Arc`
* @returns {Rac.Arc}
* @see Rac.Arc#angleDistance
*/
withAngleDistance(angleDistance) {
const newEnd = this.shiftAngle(angleDistance);
return new Arc(this.rac,
this.center, this.radius,
this.start, newEnd,
this.clockwise);
}
/**
* Returns a new `Arc` with the given `length` as the length of the
* part of the circumference it represents. This changes `end` for the
* new `Arc`.
*
* All other properties are copied from `this`.
*
* The actual `length()` of the resulting `Arc` will always be in the
* range `[0,radius*TAU)`. When the given `length` is larger that the
* circumference of the arc as a complete circle, the resulting arc length
* will be cut back into range through a modulo operation.
*
* @param {number} length - The length of the new `Arc`
* @returns {Rac.Arc}
* @see Rac.Arc#length
*/
withLength(length) {
const newAngleDistance = length / this.circumference();
return this.withAngleDistance(newAngleDistance);
}
/**
* Returns a new `Arc` with a `length()` of `this.length() * ratio`. This
* changes `end` for the new `Arc`.
*
* All other properties are copied from `this`.
*
* The actual `length()` of the resulting `Arc` will always be in the
* range *[0,radius*TAU)*. When the calculated length is larger that the
* circumference of the arc as a complete circle, the resulting arc length
* will be cut back into range through a modulo operation.
*
* @param {number} ratio - The factor to multiply `length()` by
* @returns {Rac.Arc}
*
* @see Rac.Arc#length
*/
withLengthRatio(ratio) {
const newLength = this.length() * ratio;
return this.withLength(newLength);
}
/**
* Returns a new `Arc` with `startPoint()` located at `point`. This
* changes `start` and `radius` for the new `Arc`.
*
* All other properties are copied from `this`.
*
* When `center` and `point` are considered
* [equal]{@link Rac.Point#equals}, the new `Arc` will use `this.start`.
*
* @param {Rac.Point} point - A `Point` at the `startPoint() of the new `Arc`
* @returns {Rac.Arc}
*
* @see Rac.Point#equals
*/
withStartPoint(point) {
const newStart = this.center.angleToPoint(point, this.start);
const newRadius = this.center.distanceToPoint(point);
return new Arc(this.rac,
this.center, newRadius,
newStart, this.end,
this.clockwise);
}
/**
* Returns a new `Arc` with `endPoint()` located at `point`. This changes
* `end` and `radius` in the new `Arc`.
*
* All other properties are copied from `this`.
*
* When `center` and `point` are considered
* [equal]{@link Rac.Point#equals}, the new `Arc` will use `this.end`.
*
* @param {Rac.Point} point - A `Point` at the `endPoint() of the new `Arc`
* @returns {Rac.Arc}
*
* @see Rac.Point#equals
*/
withEndPoint(point) {
const newEnd = this.center.angleToPoint(point, this.end);
const newRadius = this.center.distanceToPoint(point);
return new Arc(this.rac,
this.center, newRadius,
this.start, newEnd,
this.clockwise);
}
/**
* Returns a new `Arc` with `start` pointing towards `point` from
* `center`.
*
* All other properties are copied from `this`.
*
* When `center` and `point` are considered
* [equal]{@link Rac.Point#equals}, the new `Arc` will use `this.start`.
*
* @param {Rac.Point} point - A `Point` to point `start` towards
* @returns {Rac.Arc}
*
* @see Rac.Point#equals
*/
withStartTowardsPoint(point) {
const newStart = this.center.angleToPoint(point, this.start);
return new Arc(this.rac,
this.center, this.radius,
newStart, this.end,
this.clockwise);
}
/**
* Returns a new `Arc` with `end` pointing towards `point` from `center`.
*
* All other properties are copied from `this`.
*
* When `center` and `point` are considered
* [equal]{@link Rac.Point#equals}, the new `Arc` will use `this.end`.
*
* @param {Rac.Point} point - A `Point` to point `end` towards
* @returns {Rac.Arc}
* @see Rac.Point#equals
*/
withEndTowardsPoint(point) {
const newEnd = this.center.angleToPoint(point, this.end);
return new Arc(this.rac,
this.center, this.radius,
this.start, newEnd,
this.clockwise);
}
/**
* Returns a new `Arc` with `start` pointing towards `startPoint` and
* `end` pointing towards `endPoint`, both from `center`.
*
* All other properties are copied from `this`.
*
* * When `center` is considered [equal]{@link Rac.Point#equals} to
* either `startPoint` or `endPoint`, the new `Arc` will use `this.start`
* or `this.end` respectively.
*
* @param {Rac.Point} startPoint - A `Point` to point `start` towards
* @param {?Rac.Point} [endPoint=null] - A `Point` to point `end` towards;
* when ommited or `null`, `startPoint` is used instead
* @returns {Rac.Arc}
* @see Rac.Point#equals
*/
withAnglesTowardsPoint(startPoint, endPoint = null) {
const newStart = this.center.angleToPoint(startPoint, this.start);
const newEnd = endPoint === null
? newStart
: this.center.angleToPoint(endPoint, this.end);
return new Arc(this.rac,
this.center, this.radius,
newStart, newEnd,
this.clockwise);
}
/**
* Returns a new `Arc` with `start` shifted by the given `angle` in the
* arc's opposite orientation.
*
* All other properties are copied from `this`.
*
* Notice that this method shifts `start` to the arc's *opposite*
* orientation, intending to result in a new `Arc` with an increase to
* `angleDistance()`.
*
* @param {Rac.Angle} angle - An `Angle` to shift `start` against
* @returns {Rac.Arc}
*/
withStartExtension(angle) {
let newStart = this.start.shift(angle, !this.clockwise);
return new Arc(this.rac,
this.center, this.radius,
newStart, this.end,
this.clockwise);
}
/**
* Returns a new `Arc` with `end` shifted by the given `angle` in the
* arc's orientation.
*
* All other properties are copied from `this`.
*
* Notice that this method shifts `end` towards the arc's orientation,
* intending to result in a new `Arc` with an increase to
* `angleDistance()`.
*
* @param {Rac.Angle} angle - An `Angle` to shift `start` against
* @returns {Rac.Arc}
*/
withEndExtension(angle) {
let newEnd = this.end.shift(angle, this.clockwise);
return new Arc(this.rac,
this.center, this.radius,
this.start, newEnd,
this.clockwise);
}
/**
* Returns a new `Arc` with its `start` and `end` exchanged, and the
* opposite clockwise orientation. The center and radius remain be the
* same as `this`.
*
* @returns {Rac.Arc}
*/
reverse() {
return new Arc(this.rac,
this.center, this.radius,
this.end, this.start,
!this.clockwise);
}
/**
* Returns the given `angle` clamped to the range:
* ```
* [start + startInset, end - endInset]
* ```
* where the addition happens towards the arc's orientation, and the
* subtraction against.
*
* When `angle` is outside the range, returns whichever range limit is
* closer.
*
* When the sum of the given insets is larger that `this.arcDistance()`
* the range for the clamp is imposible to fulfill. In this case the
* returned value will be the centered between the range limits and still
* clampled to `[start, end]`.
*
* @param {Rac.Angle|number} angle - An `Angle` to clamp
* @param {Rac.Angle|number} [startInset={@link instance.Angle#zero}] - The inset
* for the lower limit of the clamping range
* @param {Rac.Angle|number} [endInset={@link instance.Angle#zero}] - The inset
* for the higher limit of the clamping range
* @returns {Rac.Angle}
*/
clampToAngles(angle, startInset = this.rac.Angle.zero, endInset = this.rac.Angle.zero) {
angle = Rac.Angle.from(this.rac, angle);
startInset = Rac.Angle.from(this.rac, startInset);
endInset = Rac.Angle.from(this.rac, endInset);
if (this.isCircle() && startInset.turn == 0 && endInset.turn == 0) {
// Complete circle
return angle;
}
// Angle in arc, with arc as origin
// All comparisons are made in a clockwise orientation
const shiftedAngle = this.distanceFromStart(angle);
const angleDistance = this.angleDistance();
const shiftedStartClamp = startInset;
const shiftedEndClamp = angleDistance.subtract(endInset);
const totalInsetTurn = startInset.turn + endInset.turn;
if (totalInsetTurn >= angleDistance.turnOne()) {
// Invalid range
const rangeDistance = shiftedEndClamp.distance(shiftedStartClamp);
let halfRange;
if (this.isCircle()) {
halfRange = rangeDistance.mult(1/2);
} else {
halfRange = totalInsetTurn >= 1
? rangeDistance.multOne(1/2)
: rangeDistance.mult(1/2);
}
const middleRange = shiftedEndClamp.add(halfRange);
const middle = this.start.shift(middleRange, this.clockwise);
return this.clampToAngles(middle);
}
if (shiftedAngle.turn >= shiftedStartClamp.turn && shiftedAngle.turn <= shiftedEndClamp.turn) {
// Inside clamp range
return angle;
}
// Outside range, figure out closest limit
let distanceToStartClamp = shiftedStartClamp.distance(shiftedAngle, false);
let distanceToEndClamp = shiftedEndClamp.distance(shiftedAngle);
if (distanceToStartClamp.turn <= distanceToEndClamp.turn) {
return this.start.shift(startInset, this.clockwise);
} else {
return this.end.shift(endInset, !this.clockwise);
}
}
/**
* Returns `true` when `angle` is between `start` and `end` in the arc's
* orientation.
*
* When the arc represents a complete circle, `true` is always returned.
*
* @param {Rac.Angle|number} angle - An `Angle` to evaluate
* @returns {boolean}
*/
containsAngle(angle) {
angle = Rac.Angle.from(this.rac, angle);
if (this.isCircle()) { return true; }
if (this.clockwise) {
let offset = angle.subtract(this.start);
let endOffset = this.end.subtract(this.start);
return offset.turn <= endOffset.turn;
} else {
let offset = angle.subtract(this.end);
let startOffset = this.start.subtract(this.end);
return offset.turn <= startOffset.turn;
}
}
/**
* Returns `true` when the projection of `point` in the arc is positioned
* between `start` and `end` in the arc's orientation.
*
* When the arc represents a complete circle, `true` is always returned.
*
* @param {Rac.Point} point - A `Point` to evaluate
* @returns {boolean}
*/
containsProjectedPoint(point) {
if (this.isCircle()) { return true; }
return this.containsAngle(this.center.angleToPoint(point));
}
/**
* Returns a new `Angle` with `angle` [shifted by]{@link Rac.Angle#shift}
* `start` in the arc's orientation.
*
* E.g.
* For a clockwise arc starting at `0.5`: `shiftAngle(0.1)` is `0.6`.
* For a counter-clockwise arc starting at `0.5`: `shiftAngle(0.1)` is `0.4`.
*
* @param {Rac.Angle|number} angle - An `Angle` to shift
* @returns {Rac.Angle}
* @see Rac.Angle#shift
*/
shiftAngle(angle) {
angle = Rac.Angle.from(this.rac, angle);
return this.start.shift(angle, this.clockwise);
}
// Returns an Angle that represents the distance from `this.start` to
// `angle` traveling in the `clockwise` orientation.
// Useful to determine for a given angle, where it sits inside the arc if
// the arc was the origin coordinate system.
//
/**
* Returns a new `Angle` that represents the angle distance from `start`
* to `angle` in the arc's orientation.
*
* E.g.
* For a clockwise arc starting at `0.5`: `distanceFromStart(0.6)` is `0.1`.
* For a counter-clockwise arc starting at `0.5`: `distanceFromStart(0.6)` is `0.9`.
*
* @param {Rac.Angle|number} angle - An `Angle` to measure the distance to
* @returns {Rac.Angle}
*/
distanceFromStart(angle) {
angle = Rac.Angle.from(this.rac, angle);
return this.start.distance(angle, this.clockwise);
}
/**
* Returns a new `Point` located in the arc at the given `angle`. This
* method does not consider the `start` nor `end` of the arc.
*
* The arc is considered a complete circle.
*
* @param {Rac.Angle|number} angle - An `Angle` towards the new `Point`
* @returns {Rac.Point}
*/
pointAtAngle(angle) {
angle = Rac.Angle.from(this.rac, angle);
return this.center.pointToAngle(angle, this.radius);
}
/**
* Returns a new `Point` located in the arc at the given `angle`
* [shifted by]{@link Rac.Angle#shift} `start` in arc's orientation.
*
* The arc is considered a complete circle.
*
* @param {Rac.Angle} angle - An `Angle` to be shifted by `start`
* @returns {Rac.Point}
*/
pointAtAngleDistance(angle) {
let shiftedAngle = this.shiftAngle(angle);
return this.pointAtAngle(shiftedAngle);
}
/**
* Returns a new `Point` located in the arc at the given `length` from
* `startPoint()` in arc's orientation.
*
* The arc is considered a complete circle.
*
* @param {number} length - The length from `startPoint()` to the new `Point`
* @returns {Rac.Point}
*/
pointAtLength(length) {
const angleDistance = length / this.circumference();
return this.pointAtAngleDistance(angleDistance);
}
/**
* Returns a new `Point` located in the arc at `length() * ratio` from
* `startPoint()` in the arc's orientation.
*
* The arc is considered a complete circle.
*
* @param {number} ratio - The factor to multiply `length()` by
* @returns {Rac.Point}
*/
pointAtLengthRatio(ratio) {
let newAngleDistance = this.angleDistance().multOne(ratio);
let shiftedAngle = this.shiftAngle(newAngleDistance);
return this.pointAtAngle(shiftedAngle);
}
/**
* Returns a new `Segment` representing the radius of the arc at the
* given `angle`. This method does not consider the `start` nor `end` of
* the arc.
*
* The arc is considered a complete circle.
*
* @param {Rac.Angle|number} angle - The direction of the radius to return
* @returns {Rac.Segment}
*/
radiusSegmentAtAngle(angle) {
angle = Rac.Angle.from(this.rac, angle);
const newRay = new Rac.Ray(this.rac, this.center, angle);
return new Rac.Segment(this.rac, newRay, this.radius);
}
/**
* Returns a new `Segment` representing the radius of the arc in the
* direction towards the given `point`. This method does not consider the
* `start` nor `end` of the arc.
*
* The arc is considered a complete circle.
*
* @param {Rac.point} point - A `Point` in the direction of the radius to return
* @returns {Rac.Segment}
*/
radiusSegmentTowardsPoint(point) {
const angle = this.center.angleToPoint(point);
const newRay = new Rac.Ray(this.rac, this.center, angle);
return new Rac.Segment(this.rac, newRay, this.radius);
}
/**
* Returns a new `Segment` for the chord formed by the intersection of
* `this` and `otherArc`, or `null` when there is no intersection.
*
* The returned `Segment` will point towards the `this` orientation.
*
* Both arcs are considered complete circles for the calculation of the
* chord, thus the endpoints of the returned segment may not lay inside
* the actual arcs.
*
* @param {Rac.Arc} otherArc - description
* @returns {?Rac.Segment}
*/
intersectionChord(otherArc) {
// https://mathworld.wolfram.com/Circle-CircleIntersection.html
// R=this, r=otherArc
if (this.center.equals(otherArc.center)) {
return null;
}
const distance = this.center.distanceToPoint(otherArc.center);
if (distance > this.radius + otherArc.radius) {
return null;
}
// distanceToChord = (d^2 - r^2 + R^2) / (d*2)
const distanceToChord = (
Math.pow(distance, 2)
- Math.pow(otherArc.radius, 2)
+ Math.pow(this.radius, 2)
) / (distance * 2);
// a = 1/d sqrt|(-d+r-R)(-d-r+R)(-d+r+R)(d+r+R)
const chordLength = (1 / distance) * Math.sqrt(
(-distance + otherArc.radius - this.radius)
* (-distance - otherArc.radius + this.radius)
* (-distance + otherArc.radius + this.radius)
* (distance + otherArc.radius + this.radius));
const segmentToChord = this.center.rayToPoint(otherArc.center)
.segment(distanceToChord);
return segmentToChord.nextSegmentPerpendicular(this.clockwise, chordLength/2)
.reverse()
.withLengthRatio(2);
}
// TODO: consider if intersectingPointsWithArc is necessary
/**
* Returns an array containing the intersecting points of `this` with
* `otherArc`.
*
* When there are no intersecting points, returns an empty array.
*
* @param {Rac.Arc} otherArc - An `Arc` to calculate intersection points with
* @returns {Rac.Arc}
*
* @ignore
*/
// intersectingPointsWithArc(otherArc) {
// let chord = this.intersectionChord(otherArc);
// if (chord === null) { return []; }
// let intersections = [chord.startPoint(), chord.endPoint()].filter(function(item) {
// return this.containsAngle(this.center.segmentToPoint(item))
// && otherArc.containsAngle(otherArc.center.segmentToPoint(item));
// }, this);
// return intersections;
// }
/**
* Returns a new `Segment` representing the chord formed by the
* intersection of the arc and 'ray', or `null` when no chord is possible.
*
* The returned `Segment` will always have the same angle as `ray`.
*
* The arc is considered a complete circle and `ray` is considered an
* unbounded line.
*
* @param {Rac.Ray} ray - A `Ray` to calculate the intersection with
* @returns {?Rac.Segment}
*/
intersectionChordWithRay(ray) {
// First check intersection
const bisector = this.center.segmentToProjectionInRay(ray);
const distance = bisector.length;
// Segment too close to center, cosine calculations may be incorrect
// Calculate segment through center
if (this.rac.equals(0, distance)) {
const start = this.pointAtAngle(ray.angle.inverse());
const newRay = new Rac.Ray(this.rac, start, ray.angle);
return new Rac.Segment(this.rac, newRay, this.radius*2);
}
// Ray is tangent, return zero-length segment at contact point
if (this.rac.equals(distance, this.radius)) {
const start = this.pointAtAngle(bisector.ray.angle);
const newRay = new Rac.Ray(this.rac, start, ray.angle);
return new Rac.Segment(this.rac, newRay, 0);
}
// Ray does not touch arc
if (distance > this.radius) {
return null;
}
const radians = Math.acos(distance/this.radius);
const angle = Rac.Angle.fromRadians(this.rac, radians);
const centerOrientation = ray.pointOrientation(this.center);
const start = this.pointAtAngle(bisector.angle().shift(angle, !centerOrientation));
const end = this.pointAtAngle(bisector.angle().shift(angle, centerOrientation));
return start.segmentToPoint(end, ray.angle);
}
/**
* Returns a new `Point` representing the end of the chord formed by the
* intersection of the arc and 'ray', or `null` when no chord is possible.
*
* When `useProjection` is `true` the method will always return a `Point`
* even when there is no contact between the arc and `ray`. In this case
* the point in the arc closest to `ray` is returned.
*
* The arc is considered a complete circle and `ray` is considered an
* unbounded line.
*
* @param {Rac.Ray} ray - A `Ray` to calculate the intersection with
* @returns {?Rac.Point}
*/
intersectionChordEndWithRay(ray, useProjection = false) {
const chord = this.intersectionChordWithRay(ray);
if (chord !== null) {
return chord.endPoint();
}
if (useProjection) {
const centerOrientation = ray.pointOrientation(this.center);
const perpendicular = ray.angle.perpendicular(!centerOrientation);
return this.pointAtAngle(perpendicular);
}
return null;
}
/**
* Returns a new `Arc` representing the section of `this` that is inside
* `otherArc`, or `null` when there is no intersection. The returned arc
* will have the same center, radius, and orientation as `this`.
*
* Both arcs are considered complete circles for the calculation of the
* intersection, thus the endpoints of the returned arc may not lay inside
* `this`.
*
* An edge case of this method is that when the distance between `this`
* and `otherArc` is the sum of their radius, meaning the arcs touch at a
* single point, the resulting arc may have a angle-distance of zero,
* which is interpreted as a complete-circle arc.
*
* @param {Rac.Arc} otherArc - An `Arc` to intersect with
* @returns {?Rac.Arc}
*/
intersectionArc(otherArc) {
const chord = this.intersectionChord(otherArc);
if (chord === null) { return null; }
return this.withAnglesTowardsPoint(chord.startPoint(), chord.endPoint());
}
// TODO: implement intersectionArcNoCircle?
// TODO: finish boundedIntersectionArc
/**
* Returns a new `Arc` representing the section of `this` that is inside
* `otherArc` and bounded by `this.start` and `this.end`, or `null` when
* there is no intersection. The returned arc will have the same center,
* radius, and orientation as `this`.
*
* `otherArc` is considered a complete circle, while the start and end of
* `this` are considered for the resulting `Arc`.
*
* When there exist two separate arc sections that intersect with
* `otherArc`: only the section of `this` closest to `start` is returned.
* This can happen when `this` starts inside `otherArc`, then exits, and
* then ends inside `otherArc`, regardless if `this` is a complete circle
* or not.
*
* @param {Rac.Arc} otherArc - An `Arc` to intersect with
* @returns {?Rac.Arc}
*
* @ignore
*/
// boundedIntersectionArc(otherArc) {
// let chord = this.intersectionChord(otherArc);
// if (chord === null) { return null; }
// let chordStartAngle = this.center.angleToPoint(chord.startPoint());
// let chordEndAngle = this.center.angleToPoint(chord.endPoint());
// // get all distances from this.start
// // if closest is chordEndAngle, only start may be inside arc
// // if closest is this.end, whole arc is inside or outside
// // if closest is chordStartAngle, only end may be inside arc
// const interStartDistance = this.start.distance(chordStartAngle, this.clockwise);
// const interEndDistance = this.start.distance(chordEndAngle, this.clockwise);
// const endDistance = this.start.distance(this.end, this.clockwise);
// // if closest is chordStartAngle, normal rules
// // if closest is end not zero, if following is chordStart, return null
// // if closest is end not zero, if following is chordend, return self
// // if closest is end zero, if following is chordStart, normal rules
// // if closest is end zero, if following is chordend, return start to chordend
// // if closest is chordEndAngle, return start to chordEnd
// if (!this.containsAngle(chordStartAngle)) {
// chordStartAngle = this.start;
// }
// if (!this.containsAngle(chordEndAngle)) {
// chordEndAngle = this.end;
// }
// return new Arc(this.rac,
// this.center, this.radius,
// chordStartAngle,
// chordEndAngle,
// this.clockwise);
// }
/**
* Returns a new `Segment` that is tangent to both `this` and `otherArc`,
* or `null` when no tangent segment is possible. The new `Segment` starts
* at the contact point with `this` and ends at the contact point with
* `otherArc`.
*
* Considering _center axis_ a ray from `this.center` towards
* `otherArc.center`, `startClockwise` determines the side of the start
* point of the returned segment in relation to _center axis_, and
* `endClockwise` the side of the end point.
*
* Both `this` and `otherArc` are considered complete circles.
*
* @param {Rac.Arc} otherArc - An `Arc` to calculate a tangent segment towards
* @param {boolean} startClockwise - The orientation of the new `Segment`
* start point in relation to the _center axis_
* @param {boolean} endClockwise - The orientation of the new `Segment`
* end point in relation to the _center axis_
* @returns {?Rac.Segment}
*/
tangentSegment(otherArc, startClockwise = true, endClockwise = true) {
if (this.center.equals(otherArc.center)) {
return null;
}
// Hypothenuse of the triangle used to calculate the tangent
// main angle is at `this.center`
const hypSegment = this.center.segmentToPoint(otherArc.center);
const ops = startClockwise === endClockwise
? otherArc.radius - this.radius
: otherArc.radius + this.radius;
// When ops and hyp are close, snap to 1
const angleSine = this.rac.equals(Math.abs(ops), hypSegment.length)
? (ops > 0 ? 1 : -1)
: ops / hypSegment.length;
if (Math.abs(angleSine) > 1) {
return null;
}
const angleRadians = Math.asin(angleSine);
const opsAngle = Rac.Angle.fromRadians(this.rac, angleRadians);
const adjOrientation = startClockwise === endClockwise
? startClockwise
: !startClockwise;
const shiftedOpsAngle = hypSegment.ray.angle.shift(opsAngle, adjOrientation);
const shiftedAdjAngle = shiftedOpsAngle.perpendicular(adjOrientation);
const startAngle = startClockwise === endClockwise
? shiftedAdjAngle
: shiftedAdjAngle.inverse()
const start = this.pointAtAngle(startAngle);
const end = otherArc.pointAtAngle(shiftedAdjAngle);
const defaultAngle = startAngle.perpendicular(!startClockwise);
return start.segmentToPoint(end, defaultAngle);
}
/**
* Returns an array containing new `Arc` objects representing `this`
* divided into `count` arcs, all with the same
* [angle distance]{@link Rac.Arc#angleDistance}.
*
* When `count` is zero or lower, returns an empty array. When `count` is
* `1` returns an arc equivalent to `this`.
*
* @param {number} count - Number of arcs to divide `this` into
* @returns {Rac.Arc[]}
*/
divideToArcs(count) {
if (count <= 0) { return []; }
const angleDistance = this.angleDistance();
const partTurn = angleDistance.turnOne() / count;
const arcs = [];
for (let index = 0; index < count; index += 1) {
const start = this.start.shift(partTurn * index, this.clockwise);
const end = this.start.shift(partTurn * (index+1), this.clockwise);
const arc = new Arc(this.rac, this.center, this.radius, start, end, this.clockwise);
arcs.push(arc);
}
return arcs;
}
/**
* Returns an array containing new `Segment` objects representing `this`
* divided into `count` chords, all with the same length.
*
* When `count` is zero or lower, returns an empty array. When `count` is
* `1` returns an arc equivalent to
* `[this.chordSegment()]{@link Rac.Arc#chordSegment}`.
*
* @param {number} count - Number of segments to divide `this` into
* @returns {Rac.Segment[]}
*/
divideToSegments(count) {
if (count <= 0) { return []; }
const angleDistance = this.angleDistance();
const partTurn = angleDistance.turnOne() / count;
const segments = [];
for (let index = 0; index < count; index += 1) {
const startAngle = this.start.shift(partTurn * index, this.clockwise);
const endAngle = this.start.shift(partTurn * (index+1), this.clockwise);
const startPoint = this.pointAtAngle(startAngle);
const endPoint = this.pointAtAngle(endAngle);
const segment = startPoint.segmentToPoint(endPoint);
segments.push(segment);
}
return segments;
}
/**
* Returns a new `Composite` that contains `Bezier` objects representing
* the arc divided into `count` beziers that approximate the shape of the
* arc.
*
* When `count` is zero or lower, returns an empty `Composite`.
*
* @param {number} count - Number of beziers to divide `this` into
* @returns {Rac.Composite}
*
* @see Rac.Bezier
*/
divideToBeziers(count) {
if (count <= 0) { return new Rac.Composite(this.rac, []); }
const angleDistance = this.angleDistance();
const partTurn = angleDistance.turnOne() / count;
// length of tangent:
// https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
const parsPerTurn = 1 / partTurn;
const tangent = this.radius * (4/3) * Math.tan(Rac.TAU/(parsPerTurn*4));
const beziers = [];
const segments = this.divideToSegments(count);
segments.forEach(item => {
const startArcRadius = this.center.segmentToPoint(item.startPoint());
const endArcRadius = this.center.segmentToPoint(item.endPoint());
let startAnchor = startArcRadius
.nextSegmentToAngleDistance(this.rac.Angle.square, !this.clockwise, tangent)
.endPoint();
let endAnchor = endArcRadius
.nextSegmentToAngleDistance(this.rac.Angle.square, this.clockwise, tangent)
.endPoint();
const newBezier = new Rac.Bezier(this.rac,
startArcRadius.endPoint(), startAnchor,
endAnchor, endArcRadius.endPoint())
beziers.push(newBezier);
});
return new Rac.Composite(this.rac, beziers);
}
} // class Arc
module.exports = Arc;