BpmnSnapping.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. import inherits from 'inherits';
  2. import {
  3. forEach
  4. } from 'min-dash';
  5. import {
  6. getBBox as getBoundingBox
  7. } from 'diagram-js/lib/util/Elements';
  8. import { is } from '../../util/ModelUtil';
  9. import { isAny } from '../modeling/util/ModelingUtil';
  10. import {
  11. isExpanded
  12. } from '../../util/DiUtil';
  13. import Snapping from 'diagram-js/lib/features/snapping/Snapping';
  14. import {
  15. mid,
  16. topLeft,
  17. bottomRight,
  18. isSnapped,
  19. setSnapped
  20. } from 'diagram-js/lib/features/snapping/SnapUtil';
  21. import {
  22. asTRBL
  23. } from 'diagram-js/lib/layout/LayoutUtil';
  24. import {
  25. getBoundaryAttachment,
  26. getParticipantSizeConstraints
  27. } from './BpmnSnappingUtil';
  28. import {
  29. getLanesRoot
  30. } from '../modeling/util/LaneUtil';
  31. var round = Math.round;
  32. var HIGH_PRIORITY = 1500;
  33. /**
  34. * BPMN specific snapping functionality
  35. *
  36. * * snap on process elements if a pool is created inside a
  37. * process diagram
  38. *
  39. * @param {EventBus} eventBus
  40. * @param {Canvas} canvas
  41. */
  42. export default function BpmnSnapping(eventBus, canvas, bpmnRules, elementRegistry) {
  43. // instantiate super
  44. Snapping.call(this, eventBus, canvas);
  45. /**
  46. * Drop participant on process <> process elements snapping
  47. */
  48. eventBus.on('create.start', function(event) {
  49. var context = event.context,
  50. shape = context.shape,
  51. rootElement = canvas.getRootElement();
  52. // snap participant around existing elements (if any)
  53. if (is(shape, 'bpmn:Participant') && is(rootElement, 'bpmn:Process')) {
  54. initParticipantSnapping(context, shape, rootElement.children);
  55. }
  56. });
  57. eventBus.on([ 'create.move', 'create.end' ], HIGH_PRIORITY, function(event) {
  58. var context = event.context,
  59. shape = context.shape,
  60. participantSnapBox = context.participantSnapBox;
  61. if (!isSnapped(event) && participantSnapBox) {
  62. snapParticipant(participantSnapBox, shape, event);
  63. }
  64. });
  65. eventBus.on('shape.move.start', function(event) {
  66. var context = event.context,
  67. shape = context.shape,
  68. rootElement = canvas.getRootElement();
  69. // snap participant around existing elements (if any)
  70. if (is(shape, 'bpmn:Participant') && is(rootElement, 'bpmn:Process')) {
  71. initParticipantSnapping(context, shape, rootElement.children);
  72. }
  73. });
  74. function canAttach(shape, target, position) {
  75. return bpmnRules.canAttach([ shape ], target, null, position) === 'attach';
  76. }
  77. function canConnect(source, target) {
  78. return bpmnRules.canConnect(source, target);
  79. }
  80. /**
  81. * Snap boundary events to elements border
  82. */
  83. eventBus.on([
  84. 'create.move',
  85. 'create.end',
  86. 'shape.move.move',
  87. 'shape.move.end'
  88. ], HIGH_PRIORITY, function(event) {
  89. var context = event.context,
  90. target = context.target,
  91. shape = context.shape;
  92. if (target && !isSnapped(event) && canAttach(shape, target, event)) {
  93. snapBoundaryEvent(event, shape, target);
  94. }
  95. });
  96. /**
  97. * Adjust parent for flowElements to the target participant
  98. * when droping onto lanes.
  99. */
  100. eventBus.on([
  101. 'shape.move.hover',
  102. 'shape.move.move',
  103. 'shape.move.end',
  104. 'create.hover',
  105. 'create.move',
  106. 'create.end'
  107. ], HIGH_PRIORITY, function(event) {
  108. var context = event.context,
  109. shape = context.shape,
  110. hover = event.hover;
  111. if (is(hover, 'bpmn:Lane') && !isAny(shape, [ 'bpmn:Lane', 'bpmn:Participant' ])) {
  112. event.hover = getLanesRoot(hover);
  113. event.hoverGfx = elementRegistry.getGraphics(event.hover);
  114. }
  115. });
  116. /**
  117. * Snap sequence flows.
  118. */
  119. eventBus.on([
  120. 'connect.move',
  121. 'connect.hover',
  122. 'connect.end'
  123. ], HIGH_PRIORITY, function(event) {
  124. var context = event.context,
  125. source = context.source,
  126. target = context.target;
  127. var connection = canConnect(source, target) || {};
  128. if (!context.initialSourcePosition) {
  129. context.initialSourcePosition = context.sourcePosition;
  130. }
  131. if (
  132. target && (
  133. connection.type === 'bpmn:Association' ||
  134. connection.type === 'bpmn:DataOutputAssociation' ||
  135. connection.type === 'bpmn:DataInputAssociation' ||
  136. connection.type === 'bpmn:SequenceFlow'
  137. )
  138. ) {
  139. // snap source
  140. context.sourcePosition = mid(source);
  141. // snap target
  142. snapToPosition(event, mid(target));
  143. } else
  144. if (connection.type === 'bpmn:MessageFlow') {
  145. if (is(source, 'bpmn:Event')) {
  146. // snap source
  147. context.sourcePosition = mid(source);
  148. }
  149. if (is(target, 'bpmn:Event')) {
  150. // snap target
  151. snapToPosition(event, mid(target));
  152. }
  153. }
  154. else {
  155. // otherwise reset source snap
  156. context.sourcePosition = context.initialSourcePosition;
  157. }
  158. });
  159. eventBus.on('resize.start', HIGH_PRIORITY, function(event) {
  160. var context = event.context,
  161. shape = context.shape;
  162. if (is(shape, 'bpmn:SubProcess') && isExpanded(shape)) {
  163. context.minDimensions = { width: 140, height: 120 };
  164. }
  165. if (is(shape, 'bpmn:Participant')) {
  166. context.minDimensions = { width: 300, height: 150 };
  167. }
  168. if (is(shape, 'bpmn:Lane') || is(shape, 'bpmn:Participant')) {
  169. context.resizeConstraints = getParticipantSizeConstraints(
  170. shape,
  171. context.direction,
  172. context.balanced
  173. );
  174. }
  175. if (is(shape, 'bpmn:TextAnnotation')) {
  176. context.minDimensions = { width: 50, height: 30 };
  177. }
  178. });
  179. }
  180. inherits(BpmnSnapping, Snapping);
  181. BpmnSnapping.$inject = [
  182. 'eventBus',
  183. 'canvas',
  184. 'bpmnRules',
  185. 'elementRegistry'
  186. ];
  187. BpmnSnapping.prototype.initSnap = function(event) {
  188. var context = event.context,
  189. shape = event.shape,
  190. shapeMid,
  191. shapeBounds,
  192. shapeTopLeft,
  193. shapeBottomRight,
  194. snapContext;
  195. snapContext = Snapping.prototype.initSnap.call(this, event);
  196. if (is(shape, 'bpmn:Participant')) {
  197. // assign higher priority for outer snaps on participants
  198. snapContext.setSnapLocations([ 'top-left', 'bottom-right', 'mid' ]);
  199. }
  200. if (shape) {
  201. shapeMid = mid(shape, event);
  202. shapeBounds = {
  203. width: shape.width,
  204. height: shape.height,
  205. x: isNaN(shape.x) ? round(shapeMid.x - shape.width / 2) : shape.x,
  206. y: isNaN(shape.y) ? round(shapeMid.y - shape.height / 2) : shape.y
  207. };
  208. shapeTopLeft = topLeft(shapeBounds);
  209. shapeBottomRight = bottomRight(shapeBounds);
  210. snapContext.setSnapOrigin('top-left', {
  211. x: shapeTopLeft.x - event.x,
  212. y: shapeTopLeft.y - event.y
  213. });
  214. snapContext.setSnapOrigin('bottom-right', {
  215. x: shapeBottomRight.x - event.x,
  216. y: shapeBottomRight.y - event.y
  217. });
  218. forEach(shape.outgoing, function(c) {
  219. var docking = c.waypoints[0];
  220. docking = docking.original || docking;
  221. snapContext.setSnapOrigin(c.id + '-docking', {
  222. x: docking.x - event.x,
  223. y: docking.y - event.y
  224. });
  225. });
  226. forEach(shape.incoming, function(c) {
  227. var docking = c.waypoints[c.waypoints.length - 1];
  228. docking = docking.original || docking;
  229. snapContext.setSnapOrigin(c.id + '-docking', {
  230. x: docking.x - event.x,
  231. y: docking.y - event.y
  232. });
  233. });
  234. }
  235. var source = context.source;
  236. if (source) {
  237. snapContext.addDefaultSnap('mid', mid(source));
  238. }
  239. };
  240. BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
  241. // use target parent as snap target
  242. if (is(shape, 'bpmn:BoundaryEvent') && shape.type !== 'label') {
  243. target = target.parent;
  244. }
  245. // add sequence flow parents as snap targets
  246. if (is(target, 'bpmn:SequenceFlow')) {
  247. this.addTargetSnaps(snapPoints, shape, target.parent);
  248. }
  249. var siblings = this.getSiblings(shape, target) || [];
  250. forEach(siblings, function(sibling) {
  251. // do not snap to lanes
  252. if (is(sibling, 'bpmn:Lane')) {
  253. return;
  254. }
  255. if (sibling.waypoints) {
  256. forEach(sibling.waypoints.slice(1, -1), function(waypoint, i) {
  257. var nextWaypoint = sibling.waypoints[i + 2],
  258. previousWaypoint = sibling.waypoints[i];
  259. if (!nextWaypoint || !previousWaypoint) {
  260. throw new Error('waypoints must exist');
  261. }
  262. if (nextWaypoint.x === waypoint.x ||
  263. nextWaypoint.y === waypoint.y ||
  264. previousWaypoint.x === waypoint.x ||
  265. previousWaypoint.y === waypoint.y) {
  266. snapPoints.add('mid', waypoint);
  267. }
  268. });
  269. return;
  270. }
  271. snapPoints.add('mid', mid(sibling));
  272. if (is(sibling, 'bpmn:Participant')) {
  273. snapPoints.add('top-left', topLeft(sibling));
  274. snapPoints.add('bottom-right', bottomRight(sibling));
  275. }
  276. });
  277. forEach(shape.incoming, function(c) {
  278. if (siblings.indexOf(c.source) === -1) {
  279. snapPoints.add('mid', mid(c.source));
  280. }
  281. var docking = c.waypoints[0];
  282. snapPoints.add(c.id + '-docking', docking.original || docking);
  283. });
  284. forEach(shape.outgoing, function(c) {
  285. if (siblings.indexOf(c.target) === -1) {
  286. snapPoints.add('mid', mid(c.target));
  287. }
  288. var docking = c.waypoints[c.waypoints.length - 1];
  289. snapPoints.add(c.id + '-docking', docking.original || docking);
  290. });
  291. };
  292. // participant snapping //////////////////////
  293. function initParticipantSnapping(context, shape, elements) {
  294. if (!elements.length) {
  295. return;
  296. }
  297. var snapBox = getBoundingBox(elements.filter(function(e) {
  298. return !e.labelTarget && !e.waypoints;
  299. }));
  300. snapBox.x -= 50;
  301. snapBox.y -= 20;
  302. snapBox.width += 70;
  303. snapBox.height += 40;
  304. // adjust shape height to include bounding box
  305. shape.width = Math.max(shape.width, snapBox.width);
  306. shape.height = Math.max(shape.height, snapBox.height);
  307. context.participantSnapBox = snapBox;
  308. }
  309. function snapParticipant(snapBox, shape, event, offset) {
  310. offset = offset || 0;
  311. var shapeHalfWidth = shape.width / 2 - offset,
  312. shapeHalfHeight = shape.height / 2;
  313. var currentTopLeft = {
  314. x: event.x - shapeHalfWidth - offset,
  315. y: event.y - shapeHalfHeight
  316. };
  317. var currentBottomRight = {
  318. x: event.x + shapeHalfWidth + offset,
  319. y: event.y + shapeHalfHeight
  320. };
  321. var snapTopLeft = snapBox,
  322. snapBottomRight = bottomRight(snapBox);
  323. if (currentTopLeft.x >= snapTopLeft.x) {
  324. setSnapped(event, 'x', snapTopLeft.x + offset + shapeHalfWidth);
  325. } else
  326. if (currentBottomRight.x <= snapBottomRight.x) {
  327. setSnapped(event, 'x', snapBottomRight.x - offset - shapeHalfWidth);
  328. }
  329. if (currentTopLeft.y >= snapTopLeft.y) {
  330. setSnapped(event, 'y', snapTopLeft.y + shapeHalfHeight);
  331. } else
  332. if (currentBottomRight.y <= snapBottomRight.y) {
  333. setSnapped(event, 'y', snapBottomRight.y - shapeHalfHeight);
  334. }
  335. }
  336. // boundary event snapping //////////////////////
  337. function snapBoundaryEvent(event, shape, target) {
  338. var targetTRBL = asTRBL(target);
  339. var direction = getBoundaryAttachment(event, target);
  340. if (/top/.test(direction)) {
  341. setSnapped(event, 'y', targetTRBL.top);
  342. } else
  343. if (/bottom/.test(direction)) {
  344. setSnapped(event, 'y', targetTRBL.bottom);
  345. }
  346. if (/left/.test(direction)) {
  347. setSnapped(event, 'x', targetTRBL.left);
  348. } else
  349. if (/right/.test(direction)) {
  350. setSnapped(event, 'x', targetTRBL.right);
  351. }
  352. }
  353. function snapToPosition(event, position) {
  354. setSnapped(event, 'x', position.x);
  355. setSnapped(event, 'y', position.y);
  356. }