LayoutUtil.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import {
  2. isObject,
  3. sortBy
  4. } from 'min-dash';
  5. import {
  6. pointDistance,
  7. pointsOnLine
  8. } from '../util/Geometry';
  9. import intersectPaths from 'path-intersection';
  10. export function roundBounds(bounds) {
  11. return {
  12. x: Math.round(bounds.x),
  13. y: Math.round(bounds.y),
  14. width: Math.round(bounds.width),
  15. height: Math.round(bounds.height)
  16. };
  17. }
  18. export function roundPoint(point) {
  19. return {
  20. x: Math.round(point.x),
  21. y: Math.round(point.y)
  22. };
  23. }
  24. /**
  25. * Convert the given bounds to a { top, left, bottom, right } descriptor.
  26. *
  27. * @param {Bounds|Point} bounds
  28. *
  29. * @return {Object}
  30. */
  31. export function asTRBL(bounds) {
  32. return {
  33. top: bounds.y,
  34. right: bounds.x + (bounds.width || 0),
  35. bottom: bounds.y + (bounds.height || 0),
  36. left: bounds.x
  37. };
  38. }
  39. /**
  40. * Convert a { top, left, bottom, right } to an objects bounds.
  41. *
  42. * @param {Object} trbl
  43. *
  44. * @return {Bounds}
  45. */
  46. export function asBounds(trbl) {
  47. return {
  48. x: trbl.left,
  49. y: trbl.top,
  50. width: trbl.right - trbl.left,
  51. height: trbl.bottom - trbl.top
  52. };
  53. }
  54. /**
  55. * Get the mid of the given bounds or point.
  56. *
  57. * @param {Bounds|Point} bounds
  58. *
  59. * @return {Point}
  60. */
  61. export function getBoundsMid(bounds) {
  62. return roundPoint({
  63. x: bounds.x + (bounds.width || 0) / 2,
  64. y: bounds.y + (bounds.height || 0) / 2
  65. });
  66. }
  67. /**
  68. * Get the mid of the given Connection.
  69. *
  70. * @param {djs.Base.Connection} connection
  71. *
  72. * @return {Point}
  73. */
  74. export function getConnectionMid(connection) {
  75. var waypoints = connection.waypoints;
  76. // calculate total length and length of each segment
  77. var parts = waypoints.reduce(function(parts, point, index) {
  78. var lastPoint = waypoints[index - 1];
  79. if (lastPoint) {
  80. var lastPart = parts[parts.length - 1];
  81. var startLength = lastPart && lastPart.endLength || 0;
  82. var length = distance(lastPoint, point);
  83. parts.push({
  84. start: lastPoint,
  85. end: point,
  86. startLength: startLength,
  87. endLength: startLength + length,
  88. length: length
  89. });
  90. }
  91. return parts;
  92. }, []);
  93. var totalLength = parts.reduce(function(length, part) {
  94. return length + part.length;
  95. }, 0);
  96. // find which segement contains middle point
  97. var midLength = totalLength / 2;
  98. var i = 0;
  99. var midSegment = parts[i];
  100. while (midSegment.endLength < midLength) {
  101. midSegment = parts[++i];
  102. }
  103. // calculate relative position on mid segment
  104. var segmentProgress = (midLength - midSegment.startLength) / midSegment.length;
  105. var midPoint = {
  106. x: midSegment.start.x + (midSegment.end.x - midSegment.start.x) * segmentProgress,
  107. y: midSegment.start.y + (midSegment.end.y - midSegment.start.y) * segmentProgress
  108. };
  109. return midPoint;
  110. }
  111. /**
  112. * Get the mid of the given Element.
  113. *
  114. * @param {djs.Base.Connection} connection
  115. *
  116. * @return {Point}
  117. */
  118. export function getMid(element) {
  119. if (isConnection(element)) {
  120. return getConnectionMid(element);
  121. }
  122. return getBoundsMid(element);
  123. }
  124. // orientation utils //////////////////////
  125. /**
  126. * Get orientation of the given rectangle with respect to
  127. * the reference rectangle.
  128. *
  129. * A padding (positive or negative) may be passed to influence
  130. * horizontal / vertical orientation and intersection.
  131. *
  132. * @param {Bounds} rect
  133. * @param {Bounds} reference
  134. * @param {Point|number} padding
  135. *
  136. * @return {string} the orientation; one of top, top-left, left, ..., bottom, right or intersect.
  137. */
  138. export function getOrientation(rect, reference, padding) {
  139. padding = padding || 0;
  140. // make sure we can use an object, too
  141. // for individual { x, y } padding
  142. if (!isObject(padding)) {
  143. padding = { x: padding, y: padding };
  144. }
  145. var rectOrientation = asTRBL(rect),
  146. referenceOrientation = asTRBL(reference);
  147. var top = rectOrientation.bottom + padding.y <= referenceOrientation.top,
  148. right = rectOrientation.left - padding.x >= referenceOrientation.right,
  149. bottom = rectOrientation.top - padding.y >= referenceOrientation.bottom,
  150. left = rectOrientation.right + padding.x <= referenceOrientation.left;
  151. var vertical = top ? 'top' : (bottom ? 'bottom' : null),
  152. horizontal = left ? 'left' : (right ? 'right' : null);
  153. if (horizontal && vertical) {
  154. return vertical + '-' + horizontal;
  155. } else {
  156. return horizontal || vertical || 'intersect';
  157. }
  158. }
  159. // intersection utils //////////////////////
  160. /**
  161. * Get intersection between an element and a line path.
  162. *
  163. * @param {PathDef} elementPath
  164. * @param {PathDef} linePath
  165. * @param {boolean} cropStart crop from start or end
  166. *
  167. * @return {Point}
  168. */
  169. export function getElementLineIntersection(elementPath, linePath, cropStart) {
  170. var intersections = getIntersections(elementPath, linePath);
  171. // recognize intersections
  172. // only one -> choose
  173. // two close together -> choose first
  174. // two or more distinct -> pull out appropriate one
  175. // none -> ok (fallback to point itself)
  176. if (intersections.length === 1) {
  177. return roundPoint(intersections[0]);
  178. } else if (intersections.length === 2 && pointDistance(intersections[0], intersections[1]) < 1) {
  179. return roundPoint(intersections[0]);
  180. } else if (intersections.length > 1) {
  181. // sort by intersections based on connection segment +
  182. // distance from start
  183. intersections = sortBy(intersections, function(i) {
  184. var distance = Math.floor(i.t2 * 100) || 1;
  185. distance = 100 - distance;
  186. distance = (distance < 10 ? '0' : '') + distance;
  187. // create a sort string that makes sure we sort
  188. // line segment ASC + line segment position DESC (for cropStart)
  189. // line segment ASC + line segment position ASC (for cropEnd)
  190. return i.segment2 + '#' + distance;
  191. });
  192. return roundPoint(intersections[cropStart ? 0 : intersections.length - 1]);
  193. }
  194. return null;
  195. }
  196. export function getIntersections(a, b) {
  197. return intersectPaths(a, b);
  198. }
  199. export function filterRedundantWaypoints(waypoints) {
  200. // alter copy of waypoints, not original
  201. waypoints = waypoints.slice();
  202. var idx = 0,
  203. point,
  204. previousPoint,
  205. nextPoint;
  206. while (waypoints[idx]) {
  207. point = waypoints[idx];
  208. previousPoint = waypoints[idx - 1];
  209. nextPoint = waypoints[idx + 1];
  210. if (pointDistance(point, nextPoint) === 0 ||
  211. pointsOnLine(previousPoint, nextPoint, point)) {
  212. // remove point, if overlapping with {nextPoint}
  213. // or on line with {previousPoint} -> {point} -> {nextPoint}
  214. waypoints.splice(idx, 1);
  215. } else {
  216. idx++;
  217. }
  218. }
  219. return waypoints;
  220. }
  221. // helpers //////////////////////
  222. function distance(a, b) {
  223. return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
  224. }
  225. function isConnection(element) {
  226. return !!element.waypoints;
  227. }