You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
5.1 KiB
229 lines
5.1 KiB
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 |
|
); |
|
}
|
|
|