ConnectionSegmentMove.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import {
  2. pointsAligned,
  3. pointsOnLine
  4. } from '../../util/Geometry';
  5. import {
  6. addSegmentDragger,
  7. getConnectionIntersection
  8. } from './BendpointUtil';
  9. import {
  10. getMid,
  11. getOrientation
  12. } from '../../layout/LayoutUtil';
  13. var MARKER_CONNECT_HOVER = 'connect-hover',
  14. MARKER_CONNECT_UPDATING = 'djs-updating';
  15. import {
  16. classes as svgClasses,
  17. remove as svgRemove
  18. } from 'tiny-svg';
  19. import {
  20. translate
  21. } from '../../util/SvgTransformUtil';
  22. function axisAdd(point, axis, delta) {
  23. return axisSet(point, axis, point[axis] + delta);
  24. }
  25. function axisSet(point, axis, value) {
  26. return {
  27. x: (axis === 'x' ? value : point.x),
  28. y: (axis === 'y' ? value : point.y)
  29. };
  30. }
  31. function axisFenced(position, segmentStart, segmentEnd, axis) {
  32. var maxValue = Math.max(segmentStart[axis], segmentEnd[axis]),
  33. minValue = Math.min(segmentStart[axis], segmentEnd[axis]);
  34. var padding = 20;
  35. var fencedValue = Math.min(Math.max(minValue + padding, position[axis]), maxValue - padding);
  36. return axisSet(segmentStart, axis, fencedValue);
  37. }
  38. function flipAxis(axis) {
  39. return axis === 'x' ? 'y' : 'x';
  40. }
  41. /**
  42. * Get the docking point on the given element.
  43. *
  44. * Compute a reasonable docking, if non exists.
  45. *
  46. * @param {Point} point
  47. * @param {djs.model.Shape} referenceElement
  48. * @param {string} moveAxis (x|y)
  49. *
  50. * @return {Point}
  51. */
  52. function getDocking(point, referenceElement, moveAxis) {
  53. var referenceMid,
  54. inverseAxis;
  55. if (point.original) {
  56. return point.original;
  57. } else {
  58. referenceMid = getMid(referenceElement);
  59. inverseAxis = flipAxis(moveAxis);
  60. return axisSet(point, inverseAxis, referenceMid[inverseAxis]);
  61. }
  62. }
  63. /**
  64. * A component that implements moving of bendpoints
  65. */
  66. export default function ConnectionSegmentMove(
  67. injector, eventBus, canvas,
  68. dragging, graphicsFactory, modeling) {
  69. // optional connection docking integration
  70. var connectionDocking = injector.get('connectionDocking', false);
  71. // API
  72. this.start = function(event, connection, idx) {
  73. var context,
  74. gfx = canvas.getGraphics(connection),
  75. segmentStartIndex = idx - 1,
  76. segmentEndIndex = idx,
  77. waypoints = connection.waypoints,
  78. segmentStart = waypoints[segmentStartIndex],
  79. segmentEnd = waypoints[segmentEndIndex],
  80. intersection = getConnectionIntersection(canvas, waypoints, event),
  81. direction, axis, dragPosition;
  82. direction = pointsAligned(segmentStart, segmentEnd);
  83. // do not move diagonal connection
  84. if (!direction) {
  85. return;
  86. }
  87. // the axis where we are going to move things
  88. axis = direction === 'v' ? 'x' : 'y';
  89. if (segmentStartIndex === 0) {
  90. segmentStart = getDocking(segmentStart, connection.source, axis);
  91. }
  92. if (segmentEndIndex === waypoints.length - 1) {
  93. segmentEnd = getDocking(segmentEnd, connection.target, axis);
  94. }
  95. if (intersection) {
  96. dragPosition = intersection.point;
  97. } else {
  98. // set to segment center as default
  99. dragPosition = {
  100. x: (segmentStart.x + segmentEnd.x) / 2,
  101. y: (segmentStart.y + segmentEnd.y) / 2
  102. };
  103. }
  104. context = {
  105. connection: connection,
  106. segmentStartIndex: segmentStartIndex,
  107. segmentEndIndex: segmentEndIndex,
  108. segmentStart: segmentStart,
  109. segmentEnd: segmentEnd,
  110. axis: axis,
  111. dragPosition: dragPosition
  112. };
  113. dragging.init(event, dragPosition, 'connectionSegment.move', {
  114. cursor: axis === 'x' ? 'resize-ew' : 'resize-ns',
  115. data: {
  116. connection: connection,
  117. connectionGfx: gfx,
  118. context: context
  119. }
  120. });
  121. };
  122. /**
  123. * Crop connection if connection cropping is provided.
  124. *
  125. * @param {Connection} connection
  126. * @param {Array<Point>} newWaypoints
  127. *
  128. * @return {Array<Point>} cropped connection waypoints
  129. */
  130. function cropConnection(connection, newWaypoints) {
  131. // crop connection, if docking service is provided only
  132. if (!connectionDocking) {
  133. return newWaypoints;
  134. }
  135. var oldWaypoints = connection.waypoints,
  136. croppedWaypoints;
  137. // temporary set new waypoints
  138. connection.waypoints = newWaypoints;
  139. croppedWaypoints = connectionDocking.getCroppedWaypoints(connection);
  140. // restore old waypoints
  141. connection.waypoints = oldWaypoints;
  142. return croppedWaypoints;
  143. }
  144. // DRAGGING IMPLEMENTATION
  145. function redrawConnection(data) {
  146. graphicsFactory.update('connection', data.connection, data.connectionGfx);
  147. }
  148. function updateDragger(context, segmentOffset, event) {
  149. var newWaypoints = context.newWaypoints,
  150. segmentStartIndex = context.segmentStartIndex + segmentOffset,
  151. segmentStart = newWaypoints[segmentStartIndex],
  152. segmentEndIndex = context.segmentEndIndex + segmentOffset,
  153. segmentEnd = newWaypoints[segmentEndIndex],
  154. axis = flipAxis(context.axis);
  155. // make sure the dragger does not move
  156. // outside the connection
  157. var draggerPosition = axisFenced(event, segmentStart, segmentEnd, axis);
  158. // update dragger
  159. translate(context.draggerGfx, draggerPosition.x, draggerPosition.y);
  160. }
  161. /**
  162. * Filter waypoints for redundant ones (i.e. on the same axis).
  163. * Returns the filtered waypoints and the offset related to the segment move.
  164. *
  165. * @param {Array<Point>} waypoints
  166. * @param {Integer} segmentStartIndex of moved segment start
  167. *
  168. * @return {Object} { filteredWaypoints, segmentOffset }
  169. */
  170. function filterRedundantWaypoints(waypoints, segmentStartIndex) {
  171. var segmentOffset = 0;
  172. var filteredWaypoints = waypoints.filter(function(r, idx) {
  173. if (pointsOnLine(waypoints[idx - 1], waypoints[idx + 1], r)) {
  174. // remove point and increment offset
  175. segmentOffset = idx <= segmentStartIndex ? segmentOffset - 1 : segmentOffset;
  176. return false;
  177. }
  178. // dont remove point
  179. return true;
  180. });
  181. return {
  182. waypoints: filteredWaypoints,
  183. segmentOffset: segmentOffset
  184. };
  185. }
  186. eventBus.on('connectionSegment.move.start', function(event) {
  187. var context = event.context,
  188. connection = event.connection,
  189. layer = canvas.getLayer('overlays');
  190. context.originalWaypoints = connection.waypoints.slice();
  191. // add dragger gfx
  192. context.draggerGfx = addSegmentDragger(layer, context.segmentStart, context.segmentEnd);
  193. svgClasses(context.draggerGfx).add('djs-dragging');
  194. canvas.addMarker(connection, MARKER_CONNECT_UPDATING);
  195. });
  196. eventBus.on('connectionSegment.move.move', function(event) {
  197. var context = event.context,
  198. connection = context.connection,
  199. segmentStartIndex = context.segmentStartIndex,
  200. segmentEndIndex = context.segmentEndIndex,
  201. segmentStart = context.segmentStart,
  202. segmentEnd = context.segmentEnd,
  203. axis = context.axis;
  204. var newWaypoints = context.originalWaypoints.slice(),
  205. newSegmentStart = axisAdd(segmentStart, axis, event['d' + axis]),
  206. newSegmentEnd = axisAdd(segmentEnd, axis, event['d' + axis]);
  207. // original waypoint count and added / removed
  208. // from start waypoint delta. We use the later
  209. // to retrieve the updated segmentStartIndex / segmentEndIndex
  210. var waypointCount = newWaypoints.length,
  211. segmentOffset = 0;
  212. // move segment start / end by axis delta
  213. newWaypoints[segmentStartIndex] = newSegmentStart;
  214. newWaypoints[segmentEndIndex] = newSegmentEnd;
  215. var sourceToSegmentOrientation,
  216. targetToSegmentOrientation;
  217. // handle first segment
  218. if (segmentStartIndex < 2) {
  219. sourceToSegmentOrientation = getOrientation(connection.source, newSegmentStart);
  220. // first bendpoint, remove first segment if intersecting
  221. if (segmentStartIndex === 1) {
  222. if (sourceToSegmentOrientation === 'intersect') {
  223. newWaypoints.shift();
  224. newWaypoints[0] = newSegmentStart;
  225. segmentOffset--;
  226. }
  227. }
  228. // docking point, add segment if not intersecting anymore
  229. else {
  230. if (sourceToSegmentOrientation !== 'intersect') {
  231. newWaypoints.unshift(segmentStart);
  232. segmentOffset++;
  233. }
  234. }
  235. }
  236. // handle last segment
  237. if (segmentEndIndex > waypointCount - 3) {
  238. targetToSegmentOrientation = getOrientation(connection.target, newSegmentEnd);
  239. // last bendpoint, remove last segment if intersecting
  240. if (segmentEndIndex === waypointCount - 2) {
  241. if (targetToSegmentOrientation === 'intersect') {
  242. newWaypoints.pop();
  243. newWaypoints[newWaypoints.length - 1] = newSegmentEnd;
  244. }
  245. }
  246. // last bendpoint, remove last segment if intersecting
  247. else {
  248. if (targetToSegmentOrientation !== 'intersect') {
  249. newWaypoints.push(segmentEnd);
  250. }
  251. }
  252. }
  253. // update connection waypoints
  254. context.newWaypoints = connection.waypoints = cropConnection(connection, newWaypoints);
  255. // update dragger position
  256. updateDragger(context, segmentOffset, event);
  257. // save segmentOffset in context
  258. context.newSegmentStartIndex = segmentStartIndex + segmentOffset;
  259. // redraw connection
  260. redrawConnection(event);
  261. });
  262. eventBus.on('connectionSegment.move.hover', function(event) {
  263. event.context.hover = event.hover;
  264. canvas.addMarker(event.hover, MARKER_CONNECT_HOVER);
  265. });
  266. eventBus.on([
  267. 'connectionSegment.move.out',
  268. 'connectionSegment.move.cleanup'
  269. ], function(event) {
  270. // remove connect marker
  271. // if it was added
  272. var hover = event.context.hover;
  273. if (hover) {
  274. canvas.removeMarker(hover, MARKER_CONNECT_HOVER);
  275. }
  276. });
  277. eventBus.on('connectionSegment.move.cleanup', function(event) {
  278. var context = event.context,
  279. connection = context.connection;
  280. // remove dragger gfx
  281. if (context.draggerGfx) {
  282. svgRemove(context.draggerGfx);
  283. }
  284. canvas.removeMarker(connection, MARKER_CONNECT_UPDATING);
  285. });
  286. eventBus.on([
  287. 'connectionSegment.move.cancel',
  288. 'connectionSegment.move.end'
  289. ], function(event) {
  290. var context = event.context,
  291. connection = context.connection;
  292. connection.waypoints = context.originalWaypoints;
  293. redrawConnection(event);
  294. });
  295. eventBus.on('connectionSegment.move.end', function(event) {
  296. var context = event.context,
  297. connection = context.connection,
  298. newWaypoints = context.newWaypoints,
  299. newSegmentStartIndex = context.newSegmentStartIndex;
  300. // ensure we have actual pixel values bendpoint
  301. // coordinates (important when zoom level was > 1 during move)
  302. newWaypoints = newWaypoints.map(function(p) {
  303. return {
  304. original: p.original,
  305. x: Math.round(p.x),
  306. y: Math.round(p.y)
  307. };
  308. });
  309. // apply filter redunant waypoints
  310. var filtered = filterRedundantWaypoints(newWaypoints, newSegmentStartIndex);
  311. // get filtered waypoints
  312. var filteredWaypoints = filtered.waypoints,
  313. croppedWaypoints = cropConnection(connection, filteredWaypoints),
  314. segmentOffset = filtered.segmentOffset;
  315. var hints = {
  316. segmentMove: {
  317. segmentStartIndex: context.segmentStartIndex,
  318. newSegmentStartIndex: newSegmentStartIndex + segmentOffset
  319. }
  320. };
  321. modeling.updateWaypoints(connection, croppedWaypoints, hints);
  322. });
  323. }
  324. ConnectionSegmentMove.$inject = [
  325. 'injector',
  326. 'eventBus',
  327. 'canvas',
  328. 'dragging',
  329. 'graphicsFactory',
  330. 'modeling'
  331. ];