AutoPlaceUtil.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import {
  2. asTRBL,
  3. getOrientation,
  4. getMid
  5. } from '../../layout/LayoutUtil';
  6. import {
  7. find,
  8. reduce
  9. } from 'min-dash';
  10. // padding to detect element placement
  11. var PLACEMENT_DETECTION_PAD = 10;
  12. export var DEFAULT_DISTANCE = 50;
  13. var DEFAULT_MAX_DISTANCE = 250;
  14. /**
  15. * Get free position starting from given position.
  16. *
  17. * @param {djs.model.Shape} source
  18. * @param {djs.model.Shape} element
  19. * @param {Point} position
  20. * @param {Function} getNextPosition
  21. *
  22. * @return {Point}
  23. */
  24. export function findFreePosition(source, element, position, getNextPosition) {
  25. var connectedAtPosition;
  26. while ((connectedAtPosition = getConnectedAtPosition(source, position, element))) {
  27. position = getNextPosition(element, position, connectedAtPosition);
  28. }
  29. return position;
  30. }
  31. /**
  32. * Returns function that returns next position.
  33. *
  34. * @param {Object} nextPositionDirection
  35. * @param {Object} [nextPositionDirection.x]
  36. * @param {Object} [nextPositionDirection.y]
  37. *
  38. * @returns {Function}
  39. */
  40. export function generateGetNextPosition(nextPositionDirection) {
  41. return function(element, previousPosition, connectedAtPosition) {
  42. var nextPosition = {
  43. x: previousPosition.x,
  44. y: previousPosition.y
  45. };
  46. [ 'x', 'y' ].forEach(function(axis) {
  47. var nextPositionDirectionForAxis = nextPositionDirection[ axis ];
  48. if (!nextPositionDirectionForAxis) {
  49. return;
  50. }
  51. var dimension = axis === 'x' ? 'width' : 'height';
  52. var margin = nextPositionDirectionForAxis.margin,
  53. minDistance = nextPositionDirectionForAxis.minDistance;
  54. if (margin < 0) {
  55. nextPosition[ axis ] = Math.min(
  56. connectedAtPosition[ axis ] + margin - element[ dimension ] / 2,
  57. previousPosition[ axis ] - minDistance + margin
  58. );
  59. } else {
  60. nextPosition[ axis ] = Math.max(
  61. connectedAtPosition[ axis ] + connectedAtPosition[ dimension ] + margin + element[ dimension ] / 2,
  62. previousPosition[ axis ] + minDistance + margin
  63. );
  64. }
  65. });
  66. return nextPosition;
  67. };
  68. }
  69. /**
  70. * Return target at given position, if defined.
  71. *
  72. * This takes connected elements from host and attachers
  73. * into account, too.
  74. */
  75. export function getConnectedAtPosition(source, position, element) {
  76. var bounds = {
  77. x: position.x - (element.width / 2),
  78. y: position.y - (element.height / 2),
  79. width: element.width,
  80. height: element.height
  81. };
  82. var closure = getAutoPlaceClosure(source, element);
  83. return find(closure, function(target) {
  84. if (target === element) {
  85. return false;
  86. }
  87. var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
  88. return orientation === 'intersect';
  89. });
  90. }
  91. /**
  92. * Compute optimal distance between source and target based on existing connections to and from source.
  93. * Assumes left-to-right and top-to-down modeling.
  94. *
  95. * @param {djs.model.Shape} source
  96. * @param {Object} [hints]
  97. * @param {number} [hints.defaultDistance]
  98. * @param {string} [hints.direction]
  99. * @param {Function} [hints.filter]
  100. * @param {Function} [hints.getWeight]
  101. * @param {number} [hints.maxDistance]
  102. * @param {string} [hints.reference]
  103. *
  104. * @return {number}
  105. */
  106. export function getConnectedDistance(source, hints) {
  107. if (!hints) {
  108. hints = {};
  109. }
  110. // targets > sources by default
  111. function getDefaultWeight(connection) {
  112. return connection.source === source ? 1 : -1;
  113. }
  114. var defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE,
  115. direction = hints.direction || 'e',
  116. filter = hints.filter,
  117. getWeight = hints.getWeight || getDefaultWeight,
  118. maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE,
  119. reference = hints.reference || 'start';
  120. if (!filter) {
  121. filter = noneFilter;
  122. }
  123. function getDistance(a, b) {
  124. if (direction === 'n') {
  125. if (reference === 'start') {
  126. return asTRBL(a).top - asTRBL(b).bottom;
  127. } else if (reference === 'center') {
  128. return asTRBL(a).top - getMid(b).y;
  129. } else {
  130. return asTRBL(a).top - asTRBL(b).top;
  131. }
  132. } else if (direction === 'w') {
  133. if (reference === 'start') {
  134. return asTRBL(a).left - asTRBL(b).right;
  135. } else if (reference === 'center') {
  136. return asTRBL(a).left - getMid(b).x;
  137. } else {
  138. return asTRBL(a).left - asTRBL(b).left;
  139. }
  140. } else if (direction === 's') {
  141. if (reference === 'start') {
  142. return asTRBL(b).top - asTRBL(a).bottom;
  143. } else if (reference === 'center') {
  144. return getMid(b).y - asTRBL(a).bottom;
  145. } else {
  146. return asTRBL(b).bottom - asTRBL(a).bottom;
  147. }
  148. } else {
  149. if (reference === 'start') {
  150. return asTRBL(b).left - asTRBL(a).right;
  151. } else if (reference === 'center') {
  152. return getMid(b).x - asTRBL(a).right;
  153. } else {
  154. return asTRBL(b).right - asTRBL(a).right;
  155. }
  156. }
  157. }
  158. var sourcesDistances = source.incoming
  159. .filter(filter)
  160. .map(function(connection) {
  161. var weight = getWeight(connection);
  162. var distance = weight < 0
  163. ? getDistance(connection.source, source)
  164. : getDistance(source, connection.source);
  165. return {
  166. id: connection.source.id,
  167. distance: distance,
  168. weight: weight
  169. };
  170. });
  171. var targetsDistances = source.outgoing
  172. .filter(filter)
  173. .map(function(connection) {
  174. var weight = getWeight(connection);
  175. var distance = weight > 0
  176. ? getDistance(source, connection.target)
  177. : getDistance(connection.target, source);
  178. return {
  179. id: connection.target.id,
  180. distance: distance,
  181. weight: weight
  182. };
  183. });
  184. var distances = sourcesDistances.concat(targetsDistances).reduce(function(accumulator, currentValue) {
  185. accumulator[ currentValue.id + '__weight_' + currentValue.weight ] = currentValue;
  186. return accumulator;
  187. }, {});
  188. var distancesGrouped = reduce(distances, function(accumulator, currentValue) {
  189. var distance = currentValue.distance,
  190. weight = currentValue.weight;
  191. if (distance < 0 || distance > maxDistance) {
  192. return accumulator;
  193. }
  194. if (!accumulator[ String(distance) ]) {
  195. accumulator[ String(distance) ] = 0;
  196. }
  197. accumulator[ String(distance) ] += 1 * weight;
  198. if (!accumulator.distance || accumulator[ accumulator.distance ] < accumulator[ String(distance) ]) {
  199. accumulator.distance = distance;
  200. }
  201. return accumulator;
  202. }, {});
  203. return distancesGrouped.distance || defaultDistance;
  204. }
  205. /**
  206. * Returns all connected elements around the given source.
  207. *
  208. * This includes:
  209. *
  210. * - connected elements
  211. * - host connected elements
  212. * - attachers connected elements
  213. *
  214. * @param {djs.model.Shape} source
  215. *
  216. * @return {Array<djs.model.Shape>}
  217. */
  218. function getAutoPlaceClosure(source) {
  219. var allConnected = getConnected(source);
  220. if (source.host) {
  221. allConnected = allConnected.concat(getConnected(source.host));
  222. }
  223. if (source.attachers) {
  224. allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) {
  225. return shapes.concat(getConnected(attacher));
  226. }, []));
  227. }
  228. return allConnected;
  229. }
  230. function getConnected(element) {
  231. return getTargets(element).concat(getSources(element));
  232. }
  233. function getSources(shape) {
  234. return shape.incoming.map(function(connection) {
  235. return connection.source;
  236. });
  237. }
  238. function getTargets(shape) {
  239. return shape.outgoing.map(function(connection) {
  240. return connection.target;
  241. });
  242. }
  243. function noneFilter() {
  244. return true;
  245. }