Snapping.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import {
  2. filter,
  3. forEach,
  4. debounce,
  5. bind
  6. } from 'min-dash';
  7. import SnapContext from './SnapContext';
  8. import {
  9. mid,
  10. isSnapped,
  11. setSnapped
  12. } from './SnapUtil';
  13. var HIGHER_PRIORITY = 1250;
  14. import {
  15. append as svgAppend,
  16. attr as svgAttr,
  17. classes as svgClasses,
  18. create as svgCreate
  19. } from 'tiny-svg';
  20. var SNAP_TOLERANCE = 7;
  21. /**
  22. * A general purpose snapping component for diagram elements.
  23. *
  24. * @param {EventBus} eventBus
  25. * @param {Canvas} canvas
  26. */
  27. export default function Snapping(eventBus, canvas) {
  28. this._canvas = canvas;
  29. var self = this;
  30. eventBus.on([ 'shape.move.start', 'create.start' ], function(event) {
  31. self.initSnap(event);
  32. });
  33. eventBus.on([ 'shape.move.move', 'shape.move.end', 'create.move', 'create.end' ], HIGHER_PRIORITY, function(event) {
  34. if (event.originalEvent && event.originalEvent.ctrlKey) {
  35. return;
  36. }
  37. if (isSnapped(event)) {
  38. return;
  39. }
  40. self.snap(event);
  41. });
  42. eventBus.on([ 'shape.move.cleanup', 'create.cleanup' ], function(event) {
  43. self.hide();
  44. });
  45. // delay hide by 1000 seconds since last match
  46. this._asyncHide = debounce(bind(this.hide, this), 1000);
  47. }
  48. Snapping.$inject = [ 'eventBus', 'canvas' ];
  49. Snapping.prototype.initSnap = function(event) {
  50. var context = event.context,
  51. shape = context.shape,
  52. snapContext = context.snapContext;
  53. if (!snapContext) {
  54. snapContext = context.snapContext = new SnapContext();
  55. }
  56. var snapMid = mid(shape, event);
  57. snapContext.setSnapOrigin('mid', {
  58. x: snapMid.x - event.x,
  59. y: snapMid.y - event.y
  60. });
  61. return snapContext;
  62. };
  63. Snapping.prototype.snap = function(event) {
  64. var context = event.context,
  65. snapContext = context.snapContext,
  66. shape = context.shape,
  67. target = context.target,
  68. snapLocations = snapContext.getSnapLocations();
  69. if (!target) {
  70. return;
  71. }
  72. var snapPoints = snapContext.pointsForTarget(target);
  73. if (!snapPoints.initialized) {
  74. this.addTargetSnaps(snapPoints, shape, target);
  75. snapPoints.initialized = true;
  76. }
  77. var snapping = {
  78. x: isSnapped(event, 'x'),
  79. y: isSnapped(event, 'y')
  80. };
  81. forEach(snapLocations, function(location) {
  82. var snapOrigin = snapContext.getSnapOrigin(location);
  83. var snapCurrent = {
  84. x: event.x + snapOrigin.x,
  85. y: event.y + snapOrigin.y
  86. };
  87. // snap on both axis, if not snapped already
  88. forEach([ 'x', 'y' ], function(axis) {
  89. var locationSnapping;
  90. if (!snapping[axis]) {
  91. locationSnapping = snapPoints.snap(snapCurrent, location, axis, SNAP_TOLERANCE);
  92. if (locationSnapping !== undefined) {
  93. snapping[axis] = {
  94. value: locationSnapping,
  95. originValue: locationSnapping - snapOrigin[axis]
  96. };
  97. }
  98. }
  99. });
  100. // no more need to snap, drop out of interation
  101. if (snapping.x && snapping.y) {
  102. return false;
  103. }
  104. });
  105. // show snap visuals
  106. this.showSnapLine('vertical', snapping.x && snapping.x.value);
  107. this.showSnapLine('horizontal', snapping.y && snapping.y.value);
  108. // adjust event { x, y, dx, dy } and mark as snapping
  109. forEach([ 'x', 'y' ], function(axis) {
  110. var axisSnapping = snapping[axis];
  111. if (typeof axisSnapping === 'object') {
  112. // set as snapped and adjust the x and/or y position of the event
  113. setSnapped(event, axis, axisSnapping.originValue);
  114. }
  115. });
  116. };
  117. Snapping.prototype._createLine = function(orientation) {
  118. var root = this._canvas.getLayer('snap');
  119. // var line = root.path('M0,0 L0,0').addClass('djs-snap-line');
  120. var line = svgCreate('path');
  121. svgAttr(line, { d: 'M0,0 L0,0' });
  122. svgClasses(line).add('djs-snap-line');
  123. svgAppend(root, line);
  124. return {
  125. update: function(position) {
  126. if (typeof position !== 'number') {
  127. svgAttr(line, { display: 'none' });
  128. } else {
  129. if (orientation === 'horizontal') {
  130. svgAttr(line, {
  131. d: 'M-100000,' + position + ' L+100000,' + position,
  132. display: ''
  133. });
  134. } else {
  135. svgAttr(line, {
  136. d: 'M ' + position + ',-100000 L ' + position + ', +100000',
  137. display: ''
  138. });
  139. }
  140. }
  141. }
  142. };
  143. };
  144. Snapping.prototype._createSnapLines = function() {
  145. this._snapLines = {
  146. horizontal: this._createLine('horizontal'),
  147. vertical: this._createLine('vertical')
  148. };
  149. };
  150. Snapping.prototype.showSnapLine = function(orientation, position) {
  151. var line = this.getSnapLine(orientation);
  152. if (line) {
  153. line.update(position);
  154. }
  155. this._asyncHide();
  156. };
  157. Snapping.prototype.getSnapLine = function(orientation) {
  158. if (!this._snapLines) {
  159. this._createSnapLines();
  160. }
  161. return this._snapLines[orientation];
  162. };
  163. Snapping.prototype.hide = function() {
  164. forEach(this._snapLines, function(l) {
  165. l.update();
  166. });
  167. };
  168. Snapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
  169. var siblings = this.getSiblings(shape, target);
  170. forEach(siblings, function(s) {
  171. snapPoints.add('mid', mid(s));
  172. });
  173. };
  174. Snapping.prototype.getSiblings = function(element, target) {
  175. // snap to all siblings that are not hidden, labels, attached to element or element itself
  176. return target && filter(target.children, function(e) {
  177. return !e.hidden && !e.labelTarget && e.host !== element && e !== element;
  178. });
  179. };