BpmnConnectSnapping.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import {
  2. mid,
  3. setSnapped
  4. } from 'diagram-js/lib/features/snapping/SnapUtil';
  5. import { isCmd } from 'diagram-js/lib/features/keyboard/KeyboardUtil';
  6. import {
  7. getOrientation
  8. } from 'diagram-js/lib/layout/LayoutUtil';
  9. import { is } from '../../util/ModelUtil';
  10. import { isAny } from '../modeling/util/ModelingUtil';
  11. import { some } from 'min-dash';
  12. /**
  13. * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
  14. *
  15. * @typedef {import('diagram-js/lib/core/EventBus').Event} Event
  16. *
  17. * @typedef {import('../../model/Types').Shape} Shape
  18. *
  19. * @typedef {import('diagram-js/lib/util/Types').Axis} Axis
  20. * @typedef {import('diagram-js/lib/util/Types').Point} Point
  21. */
  22. var HIGHER_PRIORITY = 1250;
  23. var BOUNDARY_TO_HOST_THRESHOLD = 40;
  24. var TARGET_BOUNDS_PADDING = 20,
  25. TASK_BOUNDS_PADDING = 10;
  26. var TARGET_CENTER_PADDING = 20;
  27. var AXES = [ 'x', 'y' ];
  28. var abs = Math.abs;
  29. /**
  30. * Snap during connect.
  31. *
  32. * @param {EventBus} eventBus
  33. */
  34. export default function BpmnConnectSnapping(eventBus) {
  35. eventBus.on([
  36. 'connect.hover',
  37. 'connect.move',
  38. 'connect.end',
  39. ], HIGHER_PRIORITY, function(event) {
  40. var context = event.context,
  41. canExecute = context.canExecute,
  42. start = context.start,
  43. hover = context.hover,
  44. source = context.source,
  45. target = context.target;
  46. // do NOT snap on CMD
  47. if (event.originalEvent && isCmd(event.originalEvent)) {
  48. return;
  49. }
  50. if (!context.initialConnectionStart) {
  51. context.initialConnectionStart = context.connectionStart;
  52. }
  53. // snap hover
  54. if (canExecute && hover) {
  55. snapToShape(event, hover, getTargetBoundsPadding(hover));
  56. }
  57. if (hover && isAnyType(canExecute, [
  58. 'bpmn:Association',
  59. 'bpmn:DataInputAssociation',
  60. 'bpmn:DataOutputAssociation',
  61. 'bpmn:SequenceFlow'
  62. ])) {
  63. context.connectionStart = mid(start);
  64. // snap hover
  65. if (isAny(hover, [ 'bpmn:Event', 'bpmn:Gateway' ])) {
  66. snapToPosition(event, mid(hover));
  67. }
  68. // snap hover
  69. if (isAny(hover, [ 'bpmn:Task', 'bpmn:SubProcess' ])) {
  70. snapToTargetMid(event, hover);
  71. }
  72. // snap source and target
  73. if (is(source, 'bpmn:BoundaryEvent') && target === source.host) {
  74. snapBoundaryEventLoop(event);
  75. }
  76. } else if (isType(canExecute, 'bpmn:MessageFlow')) {
  77. if (is(start, 'bpmn:Event')) {
  78. // snap start
  79. context.connectionStart = mid(start);
  80. }
  81. if (is(hover, 'bpmn:Event')) {
  82. // snap hover
  83. snapToPosition(event, mid(hover));
  84. }
  85. } else {
  86. // un-snap source
  87. context.connectionStart = context.initialConnectionStart;
  88. }
  89. });
  90. }
  91. BpmnConnectSnapping.$inject = [ 'eventBus' ];
  92. // helpers //////////
  93. /**
  94. * Snap to the given target if the event is inside the bounds of the target.
  95. *
  96. * @param {Event} event
  97. * @param {Shape} target
  98. * @param {number} padding
  99. */
  100. function snapToShape(event, target, padding) {
  101. AXES.forEach(function(axis) {
  102. var dimensionForAxis = getDimensionForAxis(axis, target);
  103. if (event[ axis ] < target[ axis ] + padding) {
  104. setSnapped(event, axis, target[ axis ] + padding);
  105. } else if (event[ axis ] > target[ axis ] + dimensionForAxis - padding) {
  106. setSnapped(event, axis, target[ axis ] + dimensionForAxis - padding);
  107. }
  108. });
  109. }
  110. /**
  111. * Snap to the target mid if the event is in the target mid.
  112. *
  113. * @param {Event} event
  114. * @param {Shape} target
  115. */
  116. function snapToTargetMid(event, target) {
  117. var targetMid = mid(target);
  118. AXES.forEach(function(axis) {
  119. if (isMid(event, target, axis)) {
  120. setSnapped(event, axis, targetMid[ axis ]);
  121. }
  122. });
  123. }
  124. /**
  125. * Snap to prevent a loop overlapping a boundary event.
  126. *
  127. * @param {Event} event
  128. */
  129. function snapBoundaryEventLoop(event) {
  130. var context = event.context,
  131. source = context.source,
  132. target = context.target;
  133. if (isReverse(context)) {
  134. return;
  135. }
  136. var sourceMid = mid(source),
  137. orientation = getOrientation(sourceMid, target, -10),
  138. axes = [];
  139. if (/top|bottom/.test(orientation)) {
  140. axes.push('x');
  141. }
  142. if (/left|right/.test(orientation)) {
  143. axes.push('y');
  144. }
  145. axes.forEach(function(axis) {
  146. var coordinate = event[ axis ], newCoordinate;
  147. if (abs(coordinate - sourceMid[ axis ]) < BOUNDARY_TO_HOST_THRESHOLD) {
  148. if (coordinate > sourceMid[ axis ]) {
  149. newCoordinate = sourceMid[ axis ] + BOUNDARY_TO_HOST_THRESHOLD;
  150. }
  151. else {
  152. newCoordinate = sourceMid[ axis ] - BOUNDARY_TO_HOST_THRESHOLD;
  153. }
  154. setSnapped(event, axis, newCoordinate);
  155. }
  156. });
  157. }
  158. /**
  159. * @param {Event} event
  160. * @param {Point} position
  161. */
  162. function snapToPosition(event, position) {
  163. setSnapped(event, 'x', position.x);
  164. setSnapped(event, 'y', position.y);
  165. }
  166. function isType(attrs, type) {
  167. return attrs && attrs.type === type;
  168. }
  169. function isAnyType(attrs, types) {
  170. return some(types, function(type) {
  171. return isType(attrs, type);
  172. });
  173. }
  174. /**
  175. * @param {Axis} axis
  176. * @param {Shape} element
  177. *
  178. * @return {number}
  179. */
  180. function getDimensionForAxis(axis, element) {
  181. return axis === 'x' ? element.width : element.height;
  182. }
  183. /**
  184. * @param {Shape} target
  185. *
  186. * @return {number}
  187. */
  188. function getTargetBoundsPadding(target) {
  189. if (is(target, 'bpmn:Task')) {
  190. return TASK_BOUNDS_PADDING;
  191. } else {
  192. return TARGET_BOUNDS_PADDING;
  193. }
  194. }
  195. /**
  196. * @param {Event} event
  197. * @param {Shape} target
  198. * @param {Axis} axis
  199. *
  200. * @return {boolean}
  201. */
  202. function isMid(event, target, axis) {
  203. return event[ axis ] > target[ axis ] + TARGET_CENTER_PADDING
  204. && event[ axis ] < target[ axis ] + getDimensionForAxis(axis, target) - TARGET_CENTER_PADDING;
  205. }
  206. function isReverse(context) {
  207. var hover = context.hover,
  208. source = context.source;
  209. return hover && source && hover === source;
  210. }