123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- var sqrt = Math.sqrt,
- min = Math.min,
- max = Math.max,
- abs = Math.abs;
- /**
- * Calculate the square (power to two) of a number.
- *
- * @param {number} n
- *
- * @return {number}
- */
- function sq(n) {
- return Math.pow(n, 2);
- }
- /**
- * Get distance between two points.
- *
- * @param {Point} p1
- * @param {Point} p2
- *
- * @return {number}
- */
- function getDistance(p1, p2) {
- return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y));
- }
- /**
- * Return the attachment of the given point on the specified line.
- *
- * The attachment is either a bendpoint (attached to the given point)
- * or segment (attached to a location on a line segment) attachment:
- *
- * ```javascript
- * var pointAttachment = {
- * type: 'bendpoint',
- * bendpointIndex: 3,
- * position: { x: 10, y: 10 } // the attach point on the line
- * };
- *
- * var segmentAttachment = {
- * type: 'segment',
- * segmentIndex: 2,
- * relativeLocation: 0.31, // attach point location between 0 (at start) and 1 (at end)
- * position: { x: 10, y: 10 } // the attach point on the line
- * };
- * ```
- *
- * @param {Point} point
- * @param {Array<Point>} line
- *
- * @return {Object} attachment
- */
- export function getAttachment(point, line) {
- var idx = 0,
- segmentStart,
- segmentEnd,
- segmentStartDistance,
- segmentEndDistance,
- attachmentPosition,
- minDistance,
- intersections,
- attachment,
- attachmentDistance,
- closestAttachmentDistance,
- closestAttachment;
- for (idx = 0; idx < line.length - 1; idx++) {
- segmentStart = line[idx];
- segmentEnd = line[idx + 1];
- if (pointsEqual(segmentStart, segmentEnd)) {
- intersections = [ segmentStart ];
- } else {
- segmentStartDistance = getDistance(point, segmentStart);
- segmentEndDistance = getDistance(point, segmentEnd);
- minDistance = min(segmentStartDistance, segmentEndDistance);
- intersections = getCircleSegmentIntersections(segmentStart, segmentEnd, point, minDistance);
- }
- if (intersections.length < 1) {
- throw new Error('expected between [1, 2] circle -> line intersections');
- }
- // one intersection -> bendpoint attachment
- if (intersections.length === 1) {
- attachment = {
- type: 'bendpoint',
- position: intersections[0],
- segmentIndex: idx,
- bendpointIndex: pointsEqual(segmentStart, intersections[0]) ? idx : idx + 1
- };
- }
- // two intersections -> segment attachment
- if (intersections.length === 2) {
- attachmentPosition = mid(intersections[0], intersections[1]);
- attachment = {
- type: 'segment',
- position: attachmentPosition,
- segmentIndex: idx,
- relativeLocation: getDistance(segmentStart, attachmentPosition) / getDistance(segmentStart, segmentEnd)
- };
- }
- attachmentDistance = getDistance(attachment.position, point);
- if (!closestAttachment || closestAttachmentDistance > attachmentDistance) {
- closestAttachment = attachment;
- closestAttachmentDistance = attachmentDistance;
- }
- }
- return closestAttachment;
- }
- /**
- * Gets the intersection between a circle and a line segment.
- *
- * @param {Point} s1 segment start
- * @param {Point} s2 segment end
- * @param {Point} cc circle center
- * @param {number} cr circle radius
- *
- * @return {Array<Point>} intersections
- */
- function getCircleSegmentIntersections(s1, s2, cc, cr) {
- var baX = s2.x - s1.x;
- var baY = s2.y - s1.y;
- var caX = cc.x - s1.x;
- var caY = cc.y - s1.y;
- var a = baX * baX + baY * baY;
- var bBy2 = baX * caX + baY * caY;
- var c = caX * caX + caY * caY - cr * cr;
- var pBy2 = bBy2 / a;
- var q = c / a;
- var disc = pBy2 * pBy2 - q;
- // check against negative value to work around
- // negative, very close to zero results (-4e-15)
- // being produced in some environments
- if (disc < 0 && disc > -0.000001) {
- disc = 0;
- }
- if (disc < 0) {
- return [];
- }
- // if disc == 0 ... dealt with later
- var tmpSqrt = sqrt(disc);
- var abScalingFactor1 = -pBy2 + tmpSqrt;
- var abScalingFactor2 = -pBy2 - tmpSqrt;
- var i1 = {
- x: s1.x - baX * abScalingFactor1,
- y: s1.y - baY * abScalingFactor1
- };
- if (disc === 0) { // abScalingFactor1 == abScalingFactor2
- return [ i1 ];
- }
- var i2 = {
- x: s1.x - baX * abScalingFactor2,
- y: s1.y - baY * abScalingFactor2
- };
- // return only points on line segment
- return [ i1, i2 ].filter(function(p) {
- return isPointInSegment(p, s1, s2);
- });
- }
- function isPointInSegment(p, segmentStart, segmentEnd) {
- return (
- fenced(p.x, segmentStart.x, segmentEnd.x) &&
- fenced(p.y, segmentStart.y, segmentEnd.y)
- );
- }
- function fenced(n, rangeStart, rangeEnd) {
- // use matching threshold to work around
- // precision errors in intersection computation
- return (
- n >= min(rangeStart, rangeEnd) - EQUAL_THRESHOLD &&
- n <= max(rangeStart, rangeEnd) + EQUAL_THRESHOLD
- );
- }
- /**
- * Calculate mid of two points.
- *
- * @param {Point} p1
- * @param {Point} p2
- *
- * @return {Point}
- */
- function mid(p1, p2) {
- return {
- x: (p1.x + p2.x) / 2,
- y: (p1.y + p2.y) / 2
- };
- }
- var EQUAL_THRESHOLD = 0.1;
- function pointsEqual(p1, p2) {
- return (
- abs(p1.x - p2.x) <= EQUAL_THRESHOLD &&
- abs(p1.y - p2.y) <= EQUAL_THRESHOLD
- );
- }
|