jumpover.js 12 KB

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