230 lines
5.1 KiB
JavaScript
230 lines
5.1 KiB
JavaScript
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
|
|
);
|
|
}
|