ManhattanLayout.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. import {
  2. isArray,
  3. find,
  4. without,
  5. assign
  6. } from 'min-dash';
  7. import {
  8. getOrientation,
  9. getMid
  10. } from './LayoutUtil';
  11. import {
  12. pointInRect,
  13. pointDistance,
  14. pointsAligned,
  15. pointsOnLine
  16. } from '../util/Geometry';
  17. var MIN_SEGMENT_LENGTH = 20,
  18. POINT_ORIENTATION_PADDING = 5;
  19. var round = Math.round;
  20. var INTERSECTION_THRESHOLD = 20,
  21. ORIENTATION_THRESHOLD = {
  22. 'h:h': 20,
  23. 'v:v': 20,
  24. 'h:v': -10,
  25. 'v:h': -10
  26. };
  27. function needsTurn(orientation, startDirection) {
  28. return !{
  29. t: /top/,
  30. r: /right/,
  31. b: /bottom/,
  32. l: /left/,
  33. h: /./,
  34. v: /./
  35. }[startDirection].test(orientation);
  36. }
  37. function canLayoutStraight(direction, targetOrientation) {
  38. return {
  39. t: /top/,
  40. r: /right/,
  41. b: /bottom/,
  42. l: /left/,
  43. h: /left|right/,
  44. v: /top|bottom/
  45. }[direction].test(targetOrientation);
  46. }
  47. function getSegmentBendpoints(a, b, directions) {
  48. var orientation = getOrientation(b, a, POINT_ORIENTATION_PADDING);
  49. var startDirection = directions.split(':')[0];
  50. var xmid = round((b.x - a.x) / 2 + a.x),
  51. ymid = round((b.y - a.y) / 2 + a.y);
  52. var segmentEnd, segmentDirections;
  53. var layoutStraight = canLayoutStraight(startDirection, orientation),
  54. layoutHorizontal = /h|r|l/.test(startDirection),
  55. layoutTurn = false;
  56. var turnNextDirections = false;
  57. if (layoutStraight) {
  58. segmentEnd = layoutHorizontal ? { x: xmid, y: a.y } : { x: a.x, y: ymid };
  59. segmentDirections = layoutHorizontal ? 'h:h' : 'v:v';
  60. } else {
  61. layoutTurn = needsTurn(orientation, startDirection);
  62. segmentDirections = layoutHorizontal ? 'h:v' : 'v:h';
  63. if (layoutTurn) {
  64. if (layoutHorizontal) {
  65. turnNextDirections = ymid === a.y;
  66. segmentEnd = {
  67. x: a.x + MIN_SEGMENT_LENGTH * (/l/.test(startDirection) ? -1 : 1),
  68. y: turnNextDirections ? ymid + MIN_SEGMENT_LENGTH : ymid
  69. };
  70. } else {
  71. turnNextDirections = xmid === a.x;
  72. segmentEnd = {
  73. x: turnNextDirections ? xmid + MIN_SEGMENT_LENGTH : xmid,
  74. y: a.y + MIN_SEGMENT_LENGTH * (/t/.test(startDirection) ? -1 : 1)
  75. };
  76. }
  77. } else {
  78. segmentEnd = {
  79. x: xmid,
  80. y: ymid
  81. };
  82. }
  83. }
  84. return {
  85. waypoints: getBendpoints(a, segmentEnd, segmentDirections).concat(segmentEnd),
  86. directions: segmentDirections,
  87. turnNextDirections: turnNextDirections
  88. };
  89. }
  90. function getStartSegment(a, b, directions) {
  91. return getSegmentBendpoints(a, b, directions);
  92. }
  93. function getEndSegment(a, b, directions) {
  94. var invertedSegment = getSegmentBendpoints(b, a, invertDirections(directions));
  95. return {
  96. waypoints: invertedSegment.waypoints.slice().reverse(),
  97. directions: invertDirections(invertedSegment.directions),
  98. turnNextDirections: invertedSegment.turnNextDirections
  99. };
  100. }
  101. function getMidSegment(startSegment, endSegment) {
  102. var startDirection = startSegment.directions.split(':')[1],
  103. endDirection = endSegment.directions.split(':')[0];
  104. if (startSegment.turnNextDirections) {
  105. startDirection = startDirection == 'h' ? 'v' : 'h';
  106. }
  107. if (endSegment.turnNextDirections) {
  108. endDirection = endDirection == 'h' ? 'v' : 'h';
  109. }
  110. var directions = startDirection + ':' + endDirection;
  111. var bendpoints = getBendpoints(
  112. startSegment.waypoints[startSegment.waypoints.length - 1],
  113. endSegment.waypoints[0],
  114. directions
  115. );
  116. return {
  117. waypoints: bendpoints,
  118. directions: directions
  119. };
  120. }
  121. function invertDirections(directions) {
  122. return directions.split(':').reverse().join(':');
  123. }
  124. /**
  125. * Handle simple layouts with maximum two bendpoints.
  126. */
  127. function getSimpleBendpoints(a, b, directions) {
  128. var xmid = round((b.x - a.x) / 2 + a.x),
  129. ymid = round((b.y - a.y) / 2 + a.y);
  130. // one point, right or left from a
  131. if (directions === 'h:v') {
  132. return [ { x: b.x, y: a.y } ];
  133. }
  134. // one point, above or below a
  135. if (directions === 'v:h') {
  136. return [ { x: a.x, y: b.y } ];
  137. }
  138. // vertical segment between a and b
  139. if (directions === 'h:h') {
  140. return [
  141. { x: xmid, y: a.y },
  142. { x: xmid, y: b.y }
  143. ];
  144. }
  145. // horizontal segment between a and b
  146. if (directions === 'v:v') {
  147. return [
  148. { x: a.x, y: ymid },
  149. { x: b.x, y: ymid }
  150. ];
  151. }
  152. throw new Error('invalid directions: can only handle varians of [hv]:[hv]');
  153. }
  154. /**
  155. * Returns the mid points for a manhattan connection between two points.
  156. *
  157. * @example h:h (horizontal:horizontal)
  158. *
  159. * [a]----[x]
  160. * |
  161. * [x]----[b]
  162. *
  163. * @example h:v (horizontal:vertical)
  164. *
  165. * [a]----[x]
  166. * |
  167. * [b]
  168. *
  169. * @example h:r (horizontal:right)
  170. *
  171. * [a]----[x]
  172. * |
  173. * [b]-[x]
  174. *
  175. * @param {Point} a
  176. * @param {Point} b
  177. * @param {String} directions
  178. *
  179. * @return {Array<Point>}
  180. */
  181. function getBendpoints(a, b, directions) {
  182. directions = directions || 'h:h';
  183. if (!isValidDirections(directions)) {
  184. throw new Error(
  185. 'unknown directions: <' + directions + '>: ' +
  186. 'must be specified as <start>:<end> ' +
  187. 'with start/end in { h,v,t,r,b,l }'
  188. );
  189. }
  190. // compute explicit directions, involving trbl dockings
  191. // using a three segmented layouting algorithm
  192. if (isExplicitDirections(directions)) {
  193. var startSegment = getStartSegment(a, b, directions),
  194. endSegment = getEndSegment(a, b, directions),
  195. midSegment = getMidSegment(startSegment, endSegment);
  196. return [].concat(
  197. startSegment.waypoints,
  198. midSegment.waypoints,
  199. endSegment.waypoints
  200. );
  201. }
  202. // handle simple [hv]:[hv] cases that can be easily computed
  203. return getSimpleBendpoints(a, b, directions);
  204. }
  205. /**
  206. * Create a connection between the two points according
  207. * to the manhattan layout (only horizontal and vertical) edges.
  208. *
  209. * @param {Point} a
  210. * @param {Point} b
  211. *
  212. * @param {String} [directions='h:h'] specifies manhattan directions for each point as {adirection}:{bdirection}.
  213. A directionfor a point is either `h` (horizontal) or `v` (vertical)
  214. *
  215. * @return {Array<Point>}
  216. */
  217. export function connectPoints(a, b, directions) {
  218. var points = getBendpoints(a, b, directions);
  219. points.unshift(a);
  220. points.push(b);
  221. return withoutRedundantPoints(points);
  222. }
  223. /**
  224. * Connect two rectangles using a manhattan layouted connection.
  225. *
  226. * @param {Bounds} source source rectangle
  227. * @param {Bounds} target target rectangle
  228. * @param {Point} [start] source docking
  229. * @param {Point} [end] target docking
  230. *
  231. * @param {Object} [hints]
  232. * @param {String} [hints.preserveDocking=source] preserve docking on selected side
  233. * @param {Array<String>} [hints.preferredLayouts]
  234. * @param {Point|Boolean} [hints.connectionStart] whether the start changed
  235. * @param {Point|Boolean} [hints.connectionEnd] whether the end changed
  236. *
  237. * @return {Array<Point>} connection points
  238. */
  239. export function connectRectangles(source, target, start, end, hints) {
  240. var preferredLayouts = hints && hints.preferredLayouts || [];
  241. var preferredLayout = without(preferredLayouts, 'straight')[0] || 'h:h';
  242. var threshold = ORIENTATION_THRESHOLD[preferredLayout] || 0;
  243. var orientation = getOrientation(source, target, threshold);
  244. var directions = getDirections(orientation, preferredLayout);
  245. start = start || getMid(source);
  246. end = end || getMid(target);
  247. var directionSplit = directions.split(':');
  248. // compute actual docking points for start / end
  249. // this ensures we properly layout only parts of the
  250. // connection that lies in between the two rectangles
  251. var startDocking = getDockingPoint(start, source, directionSplit[0], invertOrientation(orientation)),
  252. endDocking = getDockingPoint(end, target, directionSplit[1], orientation);
  253. return connectPoints(startDocking, endDocking, directions);
  254. }
  255. /**
  256. * Repair the connection between two rectangles, of which one has been updated.
  257. *
  258. * @param {Bounds} source
  259. * @param {Bounds} target
  260. * @param {Point} [start]
  261. * @param {Point} [end]
  262. * @param {Array<Point>} [waypoints]
  263. * @param {Object} [hints]
  264. * @param {Array<String>} [hints.preferredLayouts] list of preferred layouts
  265. * @param {Boolean} [hints.connectionStart]
  266. * @param {Boolean} [hints.connectionEnd]
  267. *
  268. * @return {Array<Point>} repaired waypoints
  269. */
  270. export function repairConnection(source, target, start, end, waypoints, hints) {
  271. if (isArray(start)) {
  272. waypoints = start;
  273. hints = end;
  274. start = getMid(source);
  275. end = getMid(target);
  276. }
  277. hints = assign({ preferredLayouts: [] }, hints);
  278. waypoints = waypoints || [];
  279. var preferredLayouts = hints.preferredLayouts,
  280. preferStraight = preferredLayouts.indexOf('straight') !== -1,
  281. repairedWaypoints;
  282. // just layout non-existing or simple connections
  283. // attempt to render straight lines, if required
  284. // attempt to layout a straight line
  285. repairedWaypoints = preferStraight && tryLayoutStraight(source, target, start, end, hints);
  286. if (repairedWaypoints) {
  287. return repairedWaypoints;
  288. }
  289. // try to layout from end
  290. repairedWaypoints = hints.connectionEnd && tryRepairConnectionEnd(target, source, end, waypoints);
  291. if (repairedWaypoints) {
  292. return repairedWaypoints;
  293. }
  294. // try to layout from start
  295. repairedWaypoints = hints.connectionStart && tryRepairConnectionStart(source, target, start, waypoints);
  296. if (repairedWaypoints) {
  297. return repairedWaypoints;
  298. }
  299. // or whether nothing seems to have changed
  300. if (!hints.connectionStart && !hints.connectionEnd && waypoints && waypoints.length) {
  301. return waypoints;
  302. }
  303. // simply reconnect if nothing else worked
  304. return connectRectangles(source, target, start, end, hints);
  305. }
  306. function inRange(a, start, end) {
  307. return a >= start && a <= end;
  308. }
  309. function isInRange(axis, a, b) {
  310. var size = {
  311. x: 'width',
  312. y: 'height'
  313. };
  314. return inRange(a[axis], b[axis], b[axis] + b[size[axis]]);
  315. }
  316. /**
  317. * Layout a straight connection
  318. *
  319. * @param {Bounds} source
  320. * @param {Bounds} target
  321. * @param {Point} start
  322. * @param {Point} end
  323. * @param {Object} [hints]
  324. *
  325. * @return {Array<Point>|null} waypoints if straight layout worked
  326. */
  327. export function tryLayoutStraight(source, target, start, end, hints) {
  328. var axis = {},
  329. primaryAxis,
  330. orientation;
  331. orientation = getOrientation(source, target);
  332. // only layout a straight connection if shapes are
  333. // horizontally or vertically aligned
  334. if (!/^(top|bottom|left|right)$/.test(orientation)) {
  335. return null;
  336. }
  337. if (/top|bottom/.test(orientation)) {
  338. primaryAxis = 'x';
  339. }
  340. if (/left|right/.test(orientation)) {
  341. primaryAxis = 'y';
  342. }
  343. if (hints.preserveDocking === 'target') {
  344. if (!isInRange(primaryAxis, end, source)) {
  345. return null;
  346. }
  347. axis[primaryAxis] = end[primaryAxis];
  348. return [
  349. {
  350. x: axis.x !== undefined ? axis.x : start.x,
  351. y: axis.y !== undefined ? axis.y : start.y,
  352. original: {
  353. x: axis.x !== undefined ? axis.x : start.x,
  354. y: axis.y !== undefined ? axis.y : start.y
  355. }
  356. },
  357. {
  358. x: end.x,
  359. y: end.y
  360. }
  361. ];
  362. } else {
  363. if (!isInRange(primaryAxis, start, target)) {
  364. return null;
  365. }
  366. axis[primaryAxis] = start[primaryAxis];
  367. return [
  368. {
  369. x: start.x,
  370. y: start.y
  371. },
  372. {
  373. x: axis.x !== undefined ? axis.x : end.x,
  374. y: axis.y !== undefined ? axis.y : end.y,
  375. original: {
  376. x: axis.x !== undefined ? axis.x : end.x,
  377. y: axis.y !== undefined ? axis.y : end.y
  378. }
  379. }
  380. ];
  381. }
  382. }
  383. /**
  384. * Repair a connection from start.
  385. *
  386. * @param {Bounds} moved
  387. * @param {Bounds} other
  388. * @param {Point} newDocking
  389. * @param {Array<Point>} points originalPoints from moved to other
  390. *
  391. * @return {Array<Point>|null} the repaired points between the two rectangles
  392. */
  393. function tryRepairConnectionStart(moved, other, newDocking, points) {
  394. return _tryRepairConnectionSide(moved, other, newDocking, points);
  395. }
  396. /**
  397. * Repair a connection from end.
  398. *
  399. * @param {Bounds} moved
  400. * @param {Bounds} other
  401. * @param {Point} newDocking
  402. * @param {Array<Point>} points originalPoints from moved to other
  403. *
  404. * @return {Array<Point>|null} the repaired points between the two rectangles
  405. */
  406. function tryRepairConnectionEnd(moved, other, newDocking, points) {
  407. var waypoints = points.slice().reverse();
  408. waypoints = _tryRepairConnectionSide(moved, other, newDocking, waypoints);
  409. return waypoints ? waypoints.reverse() : null;
  410. }
  411. /**
  412. * Repair a connection from one side that moved.
  413. *
  414. * @param {Bounds} moved
  415. * @param {Bounds} other
  416. * @param {Point} newDocking
  417. * @param {Array<Point>} points originalPoints from moved to other
  418. *
  419. * @return {Array<Point>} the repaired points between the two rectangles
  420. */
  421. function _tryRepairConnectionSide(moved, other, newDocking, points) {
  422. function needsRelayout(moved, other, points) {
  423. if (points.length < 3) {
  424. return true;
  425. }
  426. if (points.length > 4) {
  427. return false;
  428. }
  429. // relayout if two points overlap
  430. // this is most likely due to
  431. return !!find(points, function(p, idx) {
  432. var q = points[idx - 1];
  433. return q && pointDistance(p, q) < 3;
  434. });
  435. }
  436. function repairBendpoint(candidate, oldPeer, newPeer) {
  437. var alignment = pointsAligned(oldPeer, candidate);
  438. switch (alignment) {
  439. case 'v':
  440. // repair vertical alignment
  441. return { x: candidate.x, y: newPeer.y };
  442. case 'h':
  443. // repair horizontal alignment
  444. return { x: newPeer.x, y: candidate.y };
  445. }
  446. return { x: candidate.x, y: candidate. y };
  447. }
  448. function removeOverlapping(points, a, b) {
  449. var i;
  450. for (i = points.length - 2; i !== 0; i--) {
  451. // intersects (?) break, remove all bendpoints up to this one and relayout
  452. if (pointInRect(points[i], a, INTERSECTION_THRESHOLD) ||
  453. pointInRect(points[i], b, INTERSECTION_THRESHOLD)) {
  454. // return sliced old connection
  455. return points.slice(i);
  456. }
  457. }
  458. return points;
  459. }
  460. // (0) only repair what has layoutable bendpoints
  461. // (1) if only one bendpoint and on shape moved onto other shapes axis
  462. // (horizontally / vertically), relayout
  463. if (needsRelayout(moved, other, points)) {
  464. return null;
  465. }
  466. var oldDocking = points[0],
  467. newPoints = points.slice(),
  468. slicedPoints;
  469. // (2) repair only last line segment and only if it was layouted before
  470. newPoints[0] = newDocking;
  471. newPoints[1] = repairBendpoint(newPoints[1], oldDocking, newDocking);
  472. // (3) if shape intersects with any bendpoint after repair,
  473. // remove all segments up to this bendpoint and repair from there
  474. slicedPoints = removeOverlapping(newPoints, moved, other);
  475. if (slicedPoints !== newPoints) {
  476. return _tryRepairConnectionSide(moved, other, newDocking, slicedPoints);
  477. }
  478. return newPoints;
  479. }
  480. /**
  481. * Returns the manhattan directions connecting two rectangles
  482. * with the given orientation.
  483. *
  484. * Will always return the default layout, if it is specific
  485. * regarding sides already (trbl).
  486. *
  487. * @example
  488. *
  489. * getDirections('top'); // -> 'v:v'
  490. * getDirections('intersect'); // -> 't:t'
  491. *
  492. * getDirections('top-right', 'v:h'); // -> 'v:h'
  493. * getDirections('top-right', 'h:h'); // -> 'h:h'
  494. *
  495. *
  496. * @param {String} orientation
  497. * @param {String} defaultLayout
  498. *
  499. * @return {String}
  500. */
  501. function getDirections(orientation, defaultLayout) {
  502. // don't override specific trbl directions
  503. if (isExplicitDirections(defaultLayout)) {
  504. return defaultLayout;
  505. }
  506. switch (orientation) {
  507. case 'intersect':
  508. return 't:t';
  509. case 'top':
  510. case 'bottom':
  511. return 'v:v';
  512. case 'left':
  513. case 'right':
  514. return 'h:h';
  515. // 'top-left'
  516. // 'top-right'
  517. // 'bottom-left'
  518. // 'bottom-right'
  519. default:
  520. return defaultLayout;
  521. }
  522. }
  523. function isValidDirections(directions) {
  524. return directions && /^h|v|t|r|b|l:h|v|t|r|b|l$/.test(directions);
  525. }
  526. function isExplicitDirections(directions) {
  527. return directions && /t|r|b|l/.test(directions);
  528. }
  529. function invertOrientation(orientation) {
  530. return {
  531. 'top': 'bottom',
  532. 'bottom': 'top',
  533. 'left': 'right',
  534. 'right': 'left',
  535. 'top-left': 'bottom-right',
  536. 'bottom-right': 'top-left',
  537. 'top-right': 'bottom-left',
  538. 'bottom-left': 'top-right',
  539. }[orientation];
  540. }
  541. function getDockingPoint(point, rectangle, dockingDirection, targetOrientation) {
  542. // ensure we end up with a specific docking direction
  543. // based on the targetOrientation, if <h|v> is being passed
  544. if (dockingDirection === 'h') {
  545. dockingDirection = /left/.test(targetOrientation) ? 'l' : 'r';
  546. }
  547. if (dockingDirection === 'v') {
  548. dockingDirection = /top/.test(targetOrientation) ? 't' : 'b';
  549. }
  550. if (dockingDirection === 't') {
  551. return { original: point, x: point.x, y: rectangle.y };
  552. }
  553. if (dockingDirection === 'r') {
  554. return { original: point, x: rectangle.x + rectangle.width, y: point.y };
  555. }
  556. if (dockingDirection === 'b') {
  557. return { original: point, x: point.x, y: rectangle.y + rectangle.height };
  558. }
  559. if (dockingDirection === 'l') {
  560. return { original: point, x: rectangle.x, y: point.y };
  561. }
  562. throw new Error('unexpected dockingDirection: <' + dockingDirection + '>');
  563. }
  564. /**
  565. * Return list of waypoints with redundant ones filtered out.
  566. *
  567. * @example
  568. *
  569. * Original points:
  570. *
  571. * [x] ----- [x] ------ [x]
  572. * |
  573. * [x] ----- [x] - [x]
  574. *
  575. * Filtered:
  576. *
  577. * [x] ---------------- [x]
  578. * |
  579. * [x] ----------- [x]
  580. *
  581. * @param {Array<Point>} waypoints
  582. *
  583. * @return {Array<Point>}
  584. */
  585. export function withoutRedundantPoints(waypoints) {
  586. return waypoints.reduce(function(points, p, idx) {
  587. var previous = points[points.length - 1],
  588. next = waypoints[idx + 1];
  589. if (!pointsOnLine(previous, next, p, 0)) {
  590. points.push(p);
  591. }
  592. return points;
  593. }, []);
  594. }