LineAttachmentUtil.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /**
  2. * @typedef {import('diagram-js/lib/util/Types').Point} Point
  3. *
  4. * @typedef { {
  5. * type: 'bendpoint' | 'segment';
  6. * position: Point;
  7. * segmentIndex: number;
  8. * bendpointIndex?: number;
  9. * relativeLocation?: number;
  10. * } } Attachment
  11. */
  12. var sqrt = Math.sqrt,
  13. min = Math.min,
  14. max = Math.max,
  15. abs = Math.abs;
  16. /**
  17. * Calculate the square (power to two) of a number.
  18. *
  19. * @param {number} n
  20. *
  21. * @return {number}
  22. */
  23. function sq(n) {
  24. return Math.pow(n, 2);
  25. }
  26. /**
  27. * Get distance between two points.
  28. *
  29. * @param {Point} p1
  30. * @param {Point} p2
  31. *
  32. * @return {number}
  33. */
  34. function getDistance(p1, p2) {
  35. return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y));
  36. }
  37. /**
  38. * Return the attachment of the given point on the specified line.
  39. *
  40. * The attachment is either a bendpoint (attached to the given point)
  41. * or segment (attached to a location on a line segment) attachment:
  42. *
  43. * ```javascript
  44. * var pointAttachment = {
  45. * type: 'bendpoint',
  46. * bendpointIndex: 3,
  47. * position: { x: 10, y: 10 } // the attach point on the line
  48. * };
  49. *
  50. * var segmentAttachment = {
  51. * type: 'segment',
  52. * segmentIndex: 2,
  53. * relativeLocation: 0.31, // attach point location between 0 (at start) and 1 (at end)
  54. * position: { x: 10, y: 10 } // the attach point on the line
  55. * };
  56. * ```
  57. *
  58. * @param {Point} point
  59. * @param {Point[]} line
  60. *
  61. * @return {Attachment}
  62. */
  63. export function getAttachment(point, line) {
  64. var idx = 0,
  65. segmentStart,
  66. segmentEnd,
  67. segmentStartDistance,
  68. segmentEndDistance,
  69. attachmentPosition,
  70. minDistance,
  71. intersections,
  72. attachment,
  73. attachmentDistance,
  74. closestAttachmentDistance,
  75. closestAttachment;
  76. for (idx = 0; idx < line.length - 1; idx++) {
  77. segmentStart = line[idx];
  78. segmentEnd = line[idx + 1];
  79. if (pointsEqual(segmentStart, segmentEnd)) {
  80. intersections = [ segmentStart ];
  81. } else {
  82. segmentStartDistance = getDistance(point, segmentStart);
  83. segmentEndDistance = getDistance(point, segmentEnd);
  84. minDistance = min(segmentStartDistance, segmentEndDistance);
  85. intersections = getCircleSegmentIntersections(segmentStart, segmentEnd, point, minDistance);
  86. }
  87. if (intersections.length < 1) {
  88. throw new Error('expected between [1, 2] circle -> line intersections');
  89. }
  90. // one intersection -> bendpoint attachment
  91. if (intersections.length === 1) {
  92. attachment = {
  93. type: 'bendpoint',
  94. position: intersections[0],
  95. segmentIndex: idx,
  96. bendpointIndex: pointsEqual(segmentStart, intersections[0]) ? idx : idx + 1
  97. };
  98. }
  99. // two intersections -> segment attachment
  100. if (intersections.length === 2) {
  101. attachmentPosition = mid(intersections[0], intersections[1]);
  102. attachment = {
  103. type: 'segment',
  104. position: attachmentPosition,
  105. segmentIndex: idx,
  106. relativeLocation: getDistance(segmentStart, attachmentPosition) / getDistance(segmentStart, segmentEnd)
  107. };
  108. }
  109. attachmentDistance = getDistance(attachment.position, point);
  110. if (!closestAttachment || closestAttachmentDistance > attachmentDistance) {
  111. closestAttachment = attachment;
  112. closestAttachmentDistance = attachmentDistance;
  113. }
  114. }
  115. return closestAttachment;
  116. }
  117. /**
  118. * Get the intersection between a circle and a line segment.
  119. *
  120. * @param {Point} s1 segment start
  121. * @param {Point} s2 segment end
  122. * @param {Point} cc circle center
  123. * @param {number} cr circle radius
  124. *
  125. * @return {Point[]} intersections
  126. */
  127. function getCircleSegmentIntersections(s1, s2, cc, cr) {
  128. var baX = s2.x - s1.x;
  129. var baY = s2.y - s1.y;
  130. var caX = cc.x - s1.x;
  131. var caY = cc.y - s1.y;
  132. var a = baX * baX + baY * baY;
  133. var bBy2 = baX * caX + baY * caY;
  134. var c = caX * caX + caY * caY - cr * cr;
  135. var pBy2 = bBy2 / a;
  136. var q = c / a;
  137. var disc = pBy2 * pBy2 - q;
  138. // check against negative value to work around
  139. // negative, very close to zero results (-4e-15)
  140. // being produced in some environments
  141. if (disc < 0 && disc > -0.000001) {
  142. disc = 0;
  143. }
  144. if (disc < 0) {
  145. return [];
  146. }
  147. // if disc == 0 ... dealt with later
  148. var tmpSqrt = sqrt(disc);
  149. var abScalingFactor1 = -pBy2 + tmpSqrt;
  150. var abScalingFactor2 = -pBy2 - tmpSqrt;
  151. var i1 = {
  152. x: s1.x - baX * abScalingFactor1,
  153. y: s1.y - baY * abScalingFactor1
  154. };
  155. if (disc === 0) { // abScalingFactor1 == abScalingFactor2
  156. return [ i1 ];
  157. }
  158. var i2 = {
  159. x: s1.x - baX * abScalingFactor2,
  160. y: s1.y - baY * abScalingFactor2
  161. };
  162. // return only points on line segment
  163. return [ i1, i2 ].filter(function(p) {
  164. return isPointInSegment(p, s1, s2);
  165. });
  166. }
  167. function isPointInSegment(p, segmentStart, segmentEnd) {
  168. return (
  169. fenced(p.x, segmentStart.x, segmentEnd.x) &&
  170. fenced(p.y, segmentStart.y, segmentEnd.y)
  171. );
  172. }
  173. function fenced(n, rangeStart, rangeEnd) {
  174. // use matching threshold to work around
  175. // precision errors in intersection computation
  176. return (
  177. n >= min(rangeStart, rangeEnd) - EQUAL_THRESHOLD &&
  178. n <= max(rangeStart, rangeEnd) + EQUAL_THRESHOLD
  179. );
  180. }
  181. /**
  182. * Calculate the mid between two points.
  183. *
  184. * @param {Point} p1
  185. * @param {Point} p2
  186. *
  187. * @return {Point}
  188. */
  189. function mid(p1, p2) {
  190. return {
  191. x: (p1.x + p2.x) / 2,
  192. y: (p1.y + p2.y) / 2
  193. };
  194. }
  195. var EQUAL_THRESHOLD = 0.1;
  196. function pointsEqual(p1, p2) {
  197. return (
  198. abs(p1.x - p2.x) <= EQUAL_THRESHOLD &&
  199. abs(p1.y - p2.y) <= EQUAL_THRESHOLD
  200. );
  201. }