jumpover.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /* eslint-disable no-underscore-dangle */
  2. import { Point, Line, Path } from '@antv/x6-geometry';
  3. // takes care of math. error for case when jump is too close to end of line
  4. const CLOSE_PROXIMITY_PADDING = 1;
  5. const F13 = 1 / 3;
  6. const F23 = 2 / 3;
  7. function setupUpdating(view) {
  8. let updateList = view.graph._jumpOverUpdateList;
  9. // first time setup for this paper
  10. if (updateList == null) {
  11. updateList = view.graph._jumpOverUpdateList = [];
  12. view.graph.on('cell:mouseup', () => {
  13. const list = view.graph._jumpOverUpdateList;
  14. // add timeout to wait for the target node to be connected
  15. // fix https://github.com/antvis/X6/issues/3387
  16. setTimeout(() => {
  17. for (let i = 0; i < list.length; i += 1) {
  18. list[i].update();
  19. }
  20. });
  21. });
  22. view.graph.on('model:reseted', () => {
  23. updateList = view.graph._jumpOverUpdateList = [];
  24. });
  25. }
  26. // add this link to a list so it can be updated when some other link is updated
  27. if (updateList.indexOf(view) < 0) {
  28. updateList.push(view);
  29. // watch for change of connector type or removal of link itself
  30. // to remove the link from a list of jump over connectors
  31. const clean = () => updateList.splice(updateList.indexOf(view), 1);
  32. view.cell.once('change:connector', clean);
  33. view.cell.once('removed', clean);
  34. }
  35. }
  36. function createLines(sourcePoint, targetPoint, route = []) {
  37. const points = [sourcePoint, ...route, targetPoint];
  38. const lines = [];
  39. points.forEach((point, idx) => {
  40. const next = points[idx + 1];
  41. if (next != null) {
  42. lines.push(new Line(point, next));
  43. }
  44. });
  45. return lines;
  46. }
  47. function findLineIntersections(line, crossCheckLines) {
  48. const intersections = [];
  49. crossCheckLines.forEach((crossCheckLine) => {
  50. const intersection = line.intersectsWithLine(crossCheckLine);
  51. if (intersection) {
  52. intersections.push(intersection);
  53. }
  54. });
  55. return intersections;
  56. }
  57. function getDistence(p1, p2) {
  58. return new Line(p1, p2).squaredLength();
  59. }
  60. /**
  61. * Split input line into multiple based on intersection points.
  62. */
  63. function createJumps(line, intersections, jumpSize) {
  64. return intersections.reduce((memo, point, idx) => {
  65. // skipping points that were merged with the previous line
  66. // to make bigger arc over multiple lines that are close to each other
  67. if (skippedPoints.includes(point)) {
  68. return memo;
  69. }
  70. // always grab the last line from buffer and modify it
  71. const lastLine = memo.pop() || line;
  72. // calculate start and end of jump by moving by a given size of jump
  73. const jumpStart = Point.create(point).move(lastLine.start, -jumpSize);
  74. let jumpEnd = Point.create(point).move(lastLine.start, +jumpSize);
  75. // now try to look at the next intersection point
  76. const nextPoint = intersections[idx + 1];
  77. if (nextPoint != null) {
  78. const distance = jumpEnd.distance(nextPoint);
  79. if (distance <= jumpSize) {
  80. // next point is close enough, move the jump end by this
  81. // difference and mark the next point to be skipped
  82. jumpEnd = nextPoint.move(lastLine.start, distance);
  83. skippedPoints.push(nextPoint);
  84. }
  85. }
  86. else {
  87. // this block is inside of `else` as an optimization so the distance is
  88. // not calculated when we know there are no other intersection points
  89. const endDistance = jumpStart.distance(lastLine.end);
  90. // if the end is too close to possible jump, draw remaining line instead of a jump
  91. if (endDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) {
  92. memo.push(lastLine);
  93. return memo;
  94. }
  95. }
  96. const startDistance = jumpEnd.distance(lastLine.start);
  97. if (startDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) {
  98. // if the start of line is too close to jump, draw that line instead of a jump
  99. memo.push(lastLine);
  100. return memo;
  101. }
  102. // finally create a jump line
  103. const jumpLine = new Line(jumpStart, jumpEnd);
  104. // it's just simple line but with a `isJump` property
  105. jumppedLines.push(jumpLine);
  106. memo.push(new Line(lastLine.start, jumpStart), jumpLine, new Line(jumpEnd, lastLine.end));
  107. return memo;
  108. }, []);
  109. }
  110. function buildPath(lines, jumpSize, jumpType, radius) {
  111. const path = new Path();
  112. let segment;
  113. // first move to the start of a first line
  114. segment = Path.createSegment('M', lines[0].start);
  115. path.appendSegment(segment);
  116. lines.forEach((line, index) => {
  117. if (jumppedLines.includes(line)) {
  118. let angle;
  119. let diff;
  120. let control1;
  121. let control2;
  122. if (jumpType === 'arc') {
  123. // approximates semicircle with 2 curves
  124. angle = -90;
  125. // determine rotation of arc based on difference between points
  126. diff = line.start.diff(line.end);
  127. // make sure the arc always points up (or right)
  128. const xAxisRotate = diff.x < 0 || (diff.x === 0 && diff.y < 0);
  129. if (xAxisRotate) {
  130. angle += 180;
  131. }
  132. const center = line.getCenter();
  133. const centerLine = new Line(center, line.end).rotate(angle, center);
  134. let halfLine;
  135. // first half
  136. halfLine = new Line(line.start, center);
  137. control1 = halfLine.pointAt(2 / 3).rotate(angle, line.start);
  138. control2 = centerLine.pointAt(1 / 3).rotate(-angle, centerLine.end);
  139. segment = Path.createSegment('C', control1, control2, centerLine.end);
  140. path.appendSegment(segment);
  141. // second half
  142. halfLine = new Line(center, line.end);
  143. control1 = centerLine.pointAt(1 / 3).rotate(angle, centerLine.end);
  144. control2 = halfLine.pointAt(1 / 3).rotate(-angle, line.end);
  145. segment = Path.createSegment('C', control1, control2, line.end);
  146. path.appendSegment(segment);
  147. }
  148. else if (jumpType === 'gap') {
  149. segment = Path.createSegment('M', line.end);
  150. path.appendSegment(segment);
  151. }
  152. else if (jumpType === 'cubic') {
  153. // approximates semicircle with 1 curve
  154. angle = line.start.theta(line.end);
  155. const xOffset = jumpSize * 0.6;
  156. let yOffset = jumpSize * 1.35;
  157. // determine rotation of arc based on difference between points
  158. diff = line.start.diff(line.end);
  159. // make sure the arc always points up (or right)
  160. const xAxisRotate = diff.x < 0 || (diff.x === 0 && diff.y < 0);
  161. if (xAxisRotate) {
  162. yOffset *= -1;
  163. }
  164. control1 = new Point(line.start.x + xOffset, line.start.y + yOffset).rotate(angle, line.start);
  165. control2 = new Point(line.end.x - xOffset, line.end.y + yOffset).rotate(angle, line.end);
  166. segment = Path.createSegment('C', control1, control2, line.end);
  167. path.appendSegment(segment);
  168. }
  169. }
  170. else {
  171. const nextLine = lines[index + 1];
  172. if (radius === 0 || !nextLine || jumppedLines.includes(nextLine)) {
  173. segment = Path.createSegment('L', line.end);
  174. path.appendSegment(segment);
  175. }
  176. else {
  177. buildRoundedSegment(radius, path, line.end, line.start, nextLine.end);
  178. }
  179. }
  180. });
  181. return path;
  182. }
  183. function buildRoundedSegment(offset, path, curr, prev, next) {
  184. const prevDistance = curr.distance(prev) / 2;
  185. const nextDistance = curr.distance(next) / 2;
  186. const startMove = -Math.min(offset, prevDistance);
  187. const endMove = -Math.min(offset, nextDistance);
  188. const roundedStart = curr.clone().move(prev, startMove).round();
  189. const roundedEnd = curr.clone().move(next, endMove).round();
  190. const control1 = new Point(F13 * roundedStart.x + F23 * curr.x, F23 * curr.y + F13 * roundedStart.y);
  191. const control2 = new Point(F13 * roundedEnd.x + F23 * curr.x, F23 * curr.y + F13 * roundedEnd.y);
  192. let segment;
  193. segment = Path.createSegment('L', roundedStart);
  194. path.appendSegment(segment);
  195. segment = Path.createSegment('C', control1, control2, roundedEnd);
  196. path.appendSegment(segment);
  197. }
  198. let jumppedLines;
  199. let skippedPoints;
  200. export const jumpover = function (sourcePoint, targetPoint, routePoints, options = {}) {
  201. jumppedLines = [];
  202. skippedPoints = [];
  203. setupUpdating(this);
  204. const jumpSize = options.size || 5;
  205. const jumpType = options.type || 'arc';
  206. const radius = options.radius || 0;
  207. // list of connector types not to jump over.
  208. const ignoreConnectors = options.ignoreConnectors || ['smooth'];
  209. const graph = this.graph;
  210. const model = graph.model;
  211. const allLinks = model.getEdges();
  212. // there is just one link, draw it directly
  213. if (allLinks.length === 1) {
  214. return buildPath(createLines(sourcePoint, targetPoint, routePoints), jumpSize, jumpType, radius);
  215. }
  216. const edge = this.cell;
  217. const thisIndex = allLinks.indexOf(edge);
  218. const defaultConnector = graph.options.connecting.connector || {};
  219. // not all links are meant to be jumped over.
  220. const edges = allLinks.filter((link, idx) => {
  221. const connector = link.getConnector() || defaultConnector;
  222. // avoid jumping over links with connector type listed in `ignored connectors`.
  223. if (ignoreConnectors.includes(connector.name)) {
  224. return false;
  225. }
  226. // filter out links that are above this one and have the same connector type
  227. // otherwise there would double hoops for each intersection
  228. if (idx > thisIndex) {
  229. return connector.name !== 'jumpover';
  230. }
  231. return true;
  232. });
  233. // find views for all links
  234. const linkViews = edges.map((edge) => {
  235. return graph.findViewByCell(edge);
  236. });
  237. // create lines for this link
  238. const thisLines = createLines(sourcePoint, targetPoint, routePoints);
  239. // create lines for all other links
  240. const linkLines = linkViews.map((linkView) => {
  241. if (linkView == null) {
  242. return [];
  243. }
  244. if (linkView === this) {
  245. return thisLines;
  246. }
  247. return createLines(linkView.sourcePoint, linkView.targetPoint, linkView.routePoints);
  248. });
  249. // transform lines for this link by splitting with jump lines at
  250. // points of intersection with other links
  251. const jumpingLines = [];
  252. thisLines.forEach((line) => {
  253. // iterate all links and grab the intersections with this line
  254. // these are then sorted by distance so the line can be split more easily
  255. const intersections = edges
  256. .reduce((memo, link, i) => {
  257. // don't intersection with itself
  258. if (link !== edge) {
  259. const lineIntersections = findLineIntersections(line, linkLines[i]);
  260. memo.push(...lineIntersections);
  261. }
  262. return memo;
  263. }, [])
  264. .sort((a, b) => getDistence(line.start, a) - getDistence(line.start, b));
  265. if (intersections.length > 0) {
  266. // split the line based on found intersection points
  267. jumpingLines.push(...createJumps(line, intersections, jumpSize));
  268. }
  269. else {
  270. // without any intersection the line goes uninterrupted
  271. jumpingLines.push(line);
  272. }
  273. });
  274. const path = buildPath(jumpingLines, jumpSize, jumpType, radius);
  275. jumppedLines = [];
  276. skippedPoints = [];
  277. return options.raw ? path : path.serialize();
  278. };
  279. //# sourceMappingURL=jumpover.js.map