BpmnLayouter.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import inherits from 'inherits-browser';
  2. import {
  3. assign
  4. } from 'min-dash';
  5. import BaseLayouter from 'diagram-js/lib/layout/BaseLayouter';
  6. import {
  7. repairConnection,
  8. withoutRedundantPoints
  9. } from 'diagram-js/lib/layout/ManhattanLayout';
  10. import {
  11. getMid,
  12. getOrientation
  13. } from 'diagram-js/lib/layout/LayoutUtil';
  14. import {
  15. isExpanded
  16. } from '../../util/DiUtil';
  17. import { is } from '../../util/ModelUtil';
  18. /**
  19. * @typedef {import('diagram-js/lib/util/Types').Point} Point
  20. *
  21. * @typedef {import('../../model/Types').Connection} Connection
  22. * @typedef {import('../../model/Types').Element} Element
  23. *
  24. * @typedef {import('diagram-js/lib/layout/BaseLayouter').LayoutConnectionHints} LayoutConnectionHints
  25. *
  26. * @typedef { {
  27. * source?: Element;
  28. * target?: Element;
  29. * waypoints?: Point[];
  30. * connectionStart?: Point;
  31. * connectionEnd?: Point;
  32. * } & LayoutConnectionHints } BpmnLayoutConnectionHints
  33. */
  34. var ATTACH_ORIENTATION_PADDING = -10,
  35. BOUNDARY_TO_HOST_THRESHOLD = 40;
  36. var oppositeOrientationMapping = {
  37. 'top': 'bottom',
  38. 'top-right': 'bottom-left',
  39. 'top-left': 'bottom-right',
  40. 'right': 'left',
  41. 'bottom': 'top',
  42. 'bottom-right': 'top-left',
  43. 'bottom-left': 'top-right',
  44. 'left': 'right'
  45. };
  46. var orientationDirectionMapping = {
  47. top: 't',
  48. right: 'r',
  49. bottom: 'b',
  50. left: 'l'
  51. };
  52. export default function BpmnLayouter() {}
  53. inherits(BpmnLayouter, BaseLayouter);
  54. /**
  55. * Returns waypoints of laid out connection.
  56. *
  57. * @param {Connection} connection
  58. * @param {BpmnLayoutConnectionHints} [hints]
  59. *
  60. * @return {Point[]}
  61. */
  62. BpmnLayouter.prototype.layoutConnection = function(connection, hints) {
  63. if (!hints) {
  64. hints = {};
  65. }
  66. var source = hints.source || connection.source,
  67. target = hints.target || connection.target,
  68. waypoints = hints.waypoints || connection.waypoints,
  69. connectionStart = hints.connectionStart,
  70. connectionEnd = hints.connectionEnd;
  71. var manhattanOptions,
  72. updatedWaypoints;
  73. if (!connectionStart) {
  74. connectionStart = getConnectionDocking(waypoints && waypoints[ 0 ], source);
  75. }
  76. if (!connectionEnd) {
  77. connectionEnd = getConnectionDocking(waypoints && waypoints[ waypoints.length - 1 ], target);
  78. }
  79. // TODO(nikku): support vertical modeling
  80. // and invert preferredLayouts accordingly
  81. if (is(connection, 'bpmn:Association') ||
  82. is(connection, 'bpmn:DataAssociation')) {
  83. if (waypoints && !isCompensationAssociation(source, target)) {
  84. return [].concat([ connectionStart ], waypoints.slice(1, -1), [ connectionEnd ]);
  85. }
  86. }
  87. if (is(connection, 'bpmn:MessageFlow')) {
  88. manhattanOptions = getMessageFlowManhattanOptions(source, target);
  89. } else if (is(connection, 'bpmn:SequenceFlow') || isCompensationAssociation(source, target)) {
  90. // layout all connection between flow elements h:h, except for
  91. // (1) outgoing of boundary events -> layout based on attach orientation and target orientation
  92. // (2) incoming/outgoing of gateways -> v:h for outgoing, h:v for incoming
  93. // (3) loops
  94. if (source === target) {
  95. manhattanOptions = {
  96. preferredLayouts: getLoopPreferredLayout(source, connection)
  97. };
  98. } else if (is(source, 'bpmn:BoundaryEvent')) {
  99. manhattanOptions = {
  100. preferredLayouts: getBoundaryEventPreferredLayouts(source, target, connectionEnd)
  101. };
  102. } else if (isExpandedSubProcess(source) || isExpandedSubProcess(target)) {
  103. manhattanOptions = getSubProcessManhattanOptions(source);
  104. } else if (is(source, 'bpmn:Gateway')) {
  105. manhattanOptions = {
  106. preferredLayouts: [ 'v:h' ]
  107. };
  108. } else if (is(target, 'bpmn:Gateway')) {
  109. manhattanOptions = {
  110. preferredLayouts: [ 'h:v' ]
  111. };
  112. } else {
  113. manhattanOptions = {
  114. preferredLayouts: [ 'h:h' ]
  115. };
  116. }
  117. }
  118. if (manhattanOptions) {
  119. manhattanOptions = assign(manhattanOptions, hints);
  120. updatedWaypoints = withoutRedundantPoints(repairConnection(
  121. source,
  122. target,
  123. connectionStart,
  124. connectionEnd,
  125. waypoints,
  126. manhattanOptions
  127. ));
  128. }
  129. return updatedWaypoints || [ connectionStart, connectionEnd ];
  130. };
  131. // helpers //////////
  132. function getAttachOrientation(attachedElement) {
  133. var hostElement = attachedElement.host;
  134. return getOrientation(getMid(attachedElement), hostElement, ATTACH_ORIENTATION_PADDING);
  135. }
  136. function getMessageFlowManhattanOptions(source, target) {
  137. return {
  138. preferredLayouts: [ 'straight', 'v:v' ],
  139. preserveDocking: getMessageFlowPreserveDocking(source, target)
  140. };
  141. }
  142. function getMessageFlowPreserveDocking(source, target) {
  143. // (1) docking element connected to participant has precedence
  144. if (is(target, 'bpmn:Participant')) {
  145. return 'source';
  146. }
  147. if (is(source, 'bpmn:Participant')) {
  148. return 'target';
  149. }
  150. // (2) docking element connected to expanded sub-process has precedence
  151. if (isExpandedSubProcess(target)) {
  152. return 'source';
  153. }
  154. if (isExpandedSubProcess(source)) {
  155. return 'target';
  156. }
  157. // (3) docking event has precedence
  158. if (is(target, 'bpmn:Event')) {
  159. return 'target';
  160. }
  161. if (is(source, 'bpmn:Event')) {
  162. return 'source';
  163. }
  164. return null;
  165. }
  166. function getSubProcessManhattanOptions(source) {
  167. return {
  168. preferredLayouts: [ 'straight', 'h:h' ],
  169. preserveDocking: getSubProcessPreserveDocking(source)
  170. };
  171. }
  172. function getSubProcessPreserveDocking(source) {
  173. return isExpandedSubProcess(source) ? 'target' : 'source';
  174. }
  175. function getConnectionDocking(point, shape) {
  176. return point ? (point.original || point) : getMid(shape);
  177. }
  178. function isCompensationAssociation(source, target) {
  179. return is(target, 'bpmn:Activity') &&
  180. is(source, 'bpmn:BoundaryEvent') &&
  181. target.businessObject.isForCompensation;
  182. }
  183. function isExpandedSubProcess(element) {
  184. return is(element, 'bpmn:SubProcess') && isExpanded(element);
  185. }
  186. function isSame(a, b) {
  187. return a === b;
  188. }
  189. function isAnyOrientation(orientation, orientations) {
  190. return orientations.indexOf(orientation) !== -1;
  191. }
  192. function getHorizontalOrientation(orientation) {
  193. var matches = /right|left/.exec(orientation);
  194. return matches && matches[0];
  195. }
  196. function getVerticalOrientation(orientation) {
  197. var matches = /top|bottom/.exec(orientation);
  198. return matches && matches[0];
  199. }
  200. function isOppositeOrientation(a, b) {
  201. return oppositeOrientationMapping[a] === b;
  202. }
  203. function isOppositeHorizontalOrientation(a, b) {
  204. var horizontalOrientation = getHorizontalOrientation(a);
  205. var oppositeHorizontalOrientation = oppositeOrientationMapping[horizontalOrientation];
  206. return b.indexOf(oppositeHorizontalOrientation) !== -1;
  207. }
  208. function isOppositeVerticalOrientation(a, b) {
  209. var verticalOrientation = getVerticalOrientation(a);
  210. var oppositeVerticalOrientation = oppositeOrientationMapping[verticalOrientation];
  211. return b.indexOf(oppositeVerticalOrientation) !== -1;
  212. }
  213. function isHorizontalOrientation(orientation) {
  214. return orientation === 'right' || orientation === 'left';
  215. }
  216. function getLoopPreferredLayout(source, connection) {
  217. var waypoints = connection.waypoints;
  218. var orientation = waypoints && waypoints.length && getOrientation(waypoints[0], source);
  219. if (orientation === 'top') {
  220. return [ 't:r' ];
  221. } else if (orientation === 'right') {
  222. return [ 'r:b' ];
  223. } else if (orientation === 'left') {
  224. return [ 'l:t' ];
  225. }
  226. return [ 'b:l' ];
  227. }
  228. function getBoundaryEventPreferredLayouts(source, target, end) {
  229. var sourceMid = getMid(source),
  230. targetMid = getMid(target),
  231. attachOrientation = getAttachOrientation(source),
  232. sourceLayout,
  233. targetLayout;
  234. var isLoop = isSame(source.host, target);
  235. var attachedToSide = isAnyOrientation(attachOrientation, [ 'top', 'right', 'bottom', 'left' ]);
  236. var targetOrientation = getOrientation(targetMid, sourceMid, {
  237. x: source.width / 2 + target.width / 2,
  238. y: source.height / 2 + target.height / 2
  239. });
  240. if (isLoop) {
  241. return getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end);
  242. }
  243. // source layout
  244. sourceLayout = getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide);
  245. // target layout
  246. targetLayout = getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide);
  247. return [ sourceLayout + ':' + targetLayout ];
  248. }
  249. function getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end) {
  250. var orientation = attachedToSide ? attachOrientation : getVerticalOrientation(attachOrientation),
  251. sourceLayout = orientationDirectionMapping[ orientation ],
  252. targetLayout;
  253. if (attachedToSide) {
  254. if (isHorizontalOrientation(attachOrientation)) {
  255. targetLayout = shouldConnectToSameSide('y', source, target, end) ? 'h' : 'b';
  256. } else {
  257. targetLayout = shouldConnectToSameSide('x', source, target, end) ? 'v' : 'l';
  258. }
  259. } else {
  260. targetLayout = 'v';
  261. }
  262. return [ sourceLayout + ':' + targetLayout ];
  263. }
  264. function shouldConnectToSameSide(axis, source, target, end) {
  265. var threshold = BOUNDARY_TO_HOST_THRESHOLD;
  266. return !(
  267. areCloseOnAxis(axis, end, target, threshold) ||
  268. areCloseOnAxis(axis, end, {
  269. x: target.x + target.width,
  270. y: target.y + target.height
  271. }, threshold) ||
  272. areCloseOnAxis(axis, end, getMid(source), threshold)
  273. );
  274. }
  275. function areCloseOnAxis(axis, a, b, threshold) {
  276. return Math.abs(a[ axis ] - b[ axis ]) < threshold;
  277. }
  278. function getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide) {
  279. // attached to either top, right, bottom or left side
  280. if (attachedToSide) {
  281. return orientationDirectionMapping[ attachOrientation ];
  282. }
  283. // attached to either top-right, top-left, bottom-right or bottom-left corner
  284. // same vertical or opposite horizontal orientation
  285. if (isSame(
  286. getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)
  287. ) || isOppositeOrientation(
  288. getHorizontalOrientation(attachOrientation), getHorizontalOrientation(targetOrientation)
  289. )) {
  290. return orientationDirectionMapping[ getVerticalOrientation(attachOrientation) ];
  291. }
  292. // fallback
  293. return orientationDirectionMapping[ getHorizontalOrientation(attachOrientation) ];
  294. }
  295. function getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide) {
  296. // attached to either top, right, bottom or left side
  297. if (attachedToSide) {
  298. if (isHorizontalOrientation(attachOrientation)) {
  299. // orientation is right or left
  300. // opposite horizontal orientation or same orientation
  301. if (
  302. isOppositeHorizontalOrientation(attachOrientation, targetOrientation) ||
  303. isSame(attachOrientation, targetOrientation)
  304. ) {
  305. return 'h';
  306. }
  307. // fallback
  308. return 'v';
  309. } else {
  310. // orientation is top or bottom
  311. // opposite vertical orientation or same orientation
  312. if (
  313. isOppositeVerticalOrientation(attachOrientation, targetOrientation) ||
  314. isSame(attachOrientation, targetOrientation)
  315. ) {
  316. return 'v';
  317. }
  318. // fallback
  319. return 'h';
  320. }
  321. }
  322. // attached to either top-right, top-left, bottom-right or bottom-left corner
  323. // orientation is right, left
  324. // or same vertical orientation but also right or left
  325. if (isHorizontalOrientation(targetOrientation) ||
  326. (isSame(getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)) &&
  327. getHorizontalOrientation(targetOrientation))) {
  328. return 'h';
  329. } else {
  330. return 'v';
  331. }
  332. }