ConnectionPreview.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import {
  2. append as svgAppend,
  3. attr as svgAttr,
  4. classes as svgClasses,
  5. create as svgCreate,
  6. remove as svgRemove,
  7. clear as svgClear
  8. } from 'tiny-svg';
  9. import {
  10. isObject
  11. } from 'min-dash';
  12. import {
  13. getElementLineIntersection,
  14. getMid
  15. } from '../../layout/LayoutUtil';
  16. import {
  17. createLine
  18. } from '../../util/RenderUtil';
  19. var MARKER_CONNECTION_PREVIEW = 'djs-connection-preview';
  20. /**
  21. * Draws connection preview. Optionally, this can use layouter and connection docking to draw
  22. * better looking previews.
  23. *
  24. * @param {didi.Injector} injector
  25. * @param {Canvas} canvas
  26. * @param {GraphicsFactory} graphicsFactory
  27. * @param {ElementFactory} elementFactory
  28. */
  29. export default function ConnectionPreview(
  30. injector,
  31. canvas,
  32. graphicsFactory,
  33. elementFactory
  34. ) {
  35. this._canvas = canvas;
  36. this._graphicsFactory = graphicsFactory;
  37. this._elementFactory = elementFactory;
  38. // optional components
  39. this._connectionDocking = injector.get('connectionDocking', false);
  40. this._layouter = injector.get('layouter', false);
  41. }
  42. ConnectionPreview.$inject = [
  43. 'injector',
  44. 'canvas',
  45. 'graphicsFactory',
  46. 'elementFactory'
  47. ];
  48. /**
  49. * Draw connection preview.
  50. *
  51. * Provide at least one of <source, connectionStart> and <target, connectionEnd> to create a preview.
  52. * In the clean up stage, call `connectionPreview#cleanUp` with the context to remove preview.
  53. *
  54. * @param {Object} context
  55. * @param {Object|boolean} canConnect
  56. * @param {Object} hints
  57. * @param {djs.model.shape} [hints.source] source element
  58. * @param {djs.model.shape} [hints.target] target element
  59. * @param {Point} [hints.connectionStart] connection preview start
  60. * @param {Point} [hints.connectionEnd] connection preview end
  61. * @param {Array<Point>} [hints.waypoints] provided waypoints for preview
  62. * @param {boolean} [hints.noLayout] true if preview should not be laid out
  63. * @param {boolean} [hints.noCropping] true if preview should not be cropped
  64. * @param {boolean} [hints.noNoop] true if simple connection should not be drawn
  65. */
  66. ConnectionPreview.prototype.drawPreview = function(context, canConnect, hints) {
  67. hints = hints || {};
  68. var connectionPreviewGfx = context.connectionPreviewGfx,
  69. getConnection = context.getConnection,
  70. source = hints.source,
  71. target = hints.target,
  72. waypoints = hints.waypoints,
  73. connectionStart = hints.connectionStart,
  74. connectionEnd = hints.connectionEnd,
  75. noLayout = hints.noLayout,
  76. noCropping = hints.noCropping,
  77. noNoop = hints.noNoop,
  78. connection;
  79. var self = this;
  80. if (!connectionPreviewGfx) {
  81. connectionPreviewGfx = context.connectionPreviewGfx = this.createConnectionPreviewGfx();
  82. }
  83. svgClear(connectionPreviewGfx);
  84. if (!getConnection) {
  85. getConnection = context.getConnection = cacheReturnValues(function(canConnect, source, target) {
  86. return self.getConnection(canConnect, source, target);
  87. });
  88. }
  89. if (canConnect) {
  90. connection = getConnection(canConnect, source, target);
  91. }
  92. if (!connection) {
  93. !noNoop && this.drawNoopPreview(connectionPreviewGfx, hints);
  94. return;
  95. }
  96. connection.waypoints = waypoints || [];
  97. // optional layout
  98. if (this._layouter && !noLayout) {
  99. connection.waypoints = this._layouter.layoutConnection(connection, {
  100. source: source,
  101. target: target,
  102. connectionStart: connectionStart,
  103. connectionEnd: connectionEnd,
  104. waypoints: hints.waypoints || connection.waypoints
  105. });
  106. }
  107. // fallback if no waypoints were provided nor created with layouter
  108. if (!connection.waypoints || !connection.waypoints.length) {
  109. connection.waypoints = [
  110. source ? getMid(source) : connectionStart,
  111. target ? getMid(target) : connectionEnd
  112. ];
  113. }
  114. // optional cropping
  115. if (this._connectionDocking && (source || target) && !noCropping) {
  116. connection.waypoints = this._connectionDocking.getCroppedWaypoints(connection, source, target);
  117. }
  118. this._graphicsFactory.drawConnection(connectionPreviewGfx, connection);
  119. };
  120. /**
  121. * Draw simple connection between source and target or provided points.
  122. *
  123. * @param {SVGElement} connectionPreviewGfx container for the connection
  124. * @param {Object} hints
  125. * @param {djs.model.shape} [hints.source] source element
  126. * @param {djs.model.shape} [hints.target] target element
  127. * @param {Point} [hints.connectionStart] required if source is not provided
  128. * @param {Point} [hints.connectionEnd] required if target is not provided
  129. */
  130. ConnectionPreview.prototype.drawNoopPreview = function(connectionPreviewGfx, hints) {
  131. var source = hints.source,
  132. target = hints.target,
  133. start = hints.connectionStart || getMid(source),
  134. end = hints.connectionEnd || getMid(target);
  135. var waypoints = this.cropWaypoints(start, end, source, target);
  136. var connection = this.createNoopConnection(waypoints[0], waypoints[1]);
  137. svgAppend(connectionPreviewGfx, connection);
  138. };
  139. /**
  140. * Return cropped waypoints.
  141. *
  142. * @param {Point} start
  143. * @param {Point} end
  144. * @param {djs.model.shape} source
  145. * @param {djs.model.shape} target
  146. *
  147. * @returns {Array}
  148. */
  149. ConnectionPreview.prototype.cropWaypoints = function(start, end, source, target) {
  150. var graphicsFactory = this._graphicsFactory,
  151. sourcePath = source && graphicsFactory.getShapePath(source),
  152. targetPath = target && graphicsFactory.getShapePath(target),
  153. connectionPath = graphicsFactory.getConnectionPath({ waypoints: [ start, end ] });
  154. start = (source && getElementLineIntersection(sourcePath, connectionPath, true)) || start;
  155. end = (target && getElementLineIntersection(targetPath, connectionPath, false)) || end;
  156. return [ start, end ];
  157. };
  158. /**
  159. * Remove connection preview container if it exists.
  160. *
  161. * @param {Object} [context]
  162. * @param {SVGElement} [context.connectionPreviewGfx] preview container
  163. */
  164. ConnectionPreview.prototype.cleanUp = function(context) {
  165. if (context && context.connectionPreviewGfx) {
  166. svgRemove(context.connectionPreviewGfx);
  167. }
  168. };
  169. /**
  170. * Get connection that connects source and target.
  171. *
  172. * @param {Object|boolean} canConnect
  173. *
  174. * @returns {djs.model.connection}
  175. */
  176. ConnectionPreview.prototype.getConnection = function(canConnect) {
  177. var attrs = ensureConnectionAttrs(canConnect);
  178. return this._elementFactory.createConnection(attrs);
  179. };
  180. /**
  181. * Add and return preview graphics.
  182. *
  183. * @returns {SVGElement}
  184. */
  185. ConnectionPreview.prototype.createConnectionPreviewGfx = function() {
  186. var gfx = svgCreate('g');
  187. svgAttr(gfx, {
  188. pointerEvents: 'none'
  189. });
  190. svgClasses(gfx).add(MARKER_CONNECTION_PREVIEW);
  191. svgAppend(this._canvas.getActiveLayer(), gfx);
  192. return gfx;
  193. };
  194. /**
  195. * Create and return simple connection.
  196. *
  197. * @param {Point} start
  198. * @param {Point} end
  199. *
  200. * @returns {SVGElement}
  201. */
  202. ConnectionPreview.prototype.createNoopConnection = function(start, end) {
  203. return createLine([ start, end ], {
  204. 'stroke': '#333',
  205. 'strokeDasharray': [ 1 ],
  206. 'strokeWidth': 2,
  207. 'pointer-events': 'none'
  208. });
  209. };
  210. // helpers //////////
  211. /**
  212. * Returns function that returns cached return values referenced by stringified first argument.
  213. *
  214. * @param {Function} fn
  215. *
  216. * @return {Function}
  217. */
  218. function cacheReturnValues(fn) {
  219. var returnValues = {};
  220. /**
  221. * Return cached return value referenced by stringified first argument.
  222. *
  223. * @returns {*}
  224. */
  225. return function(firstArgument) {
  226. var key = JSON.stringify(firstArgument);
  227. var returnValue = returnValues[key];
  228. if (!returnValue) {
  229. returnValue = returnValues[key] = fn.apply(null, arguments);
  230. }
  231. return returnValue;
  232. };
  233. }
  234. /**
  235. * Ensure connection attributes is object.
  236. *
  237. * @param {Object|boolean} canConnect
  238. *
  239. * @returns {Object}
  240. */
  241. function ensureConnectionAttrs(canConnect) {
  242. if (isObject(canConnect)) {
  243. return canConnect;
  244. } else {
  245. return {};
  246. }
  247. }