YmAutoPlaceUtil.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // @ts-nocheck
  2. import { asTRBL, getOrientation, getMid } from 'diagram-js/lib/layout/LayoutUtil';
  3. import { find, reduce } from 'min-dash';
  4. import { Shape } from 'diagram-js/lib/model';
  5. import { Point } from 'diagram-js/lib/util/Types';
  6. import { DEFAULT_DISTANCE } from '../config/constants';
  7. // padding to detect element placement
  8. const PLACEMENT_DETECTION_PAD = 10;
  9. const DEFAULT_MAX_DISTANCE = 350;
  10. /**
  11. * Get free position starting from given position.
  12. *
  13. * @param {Shape} source
  14. * @param {Shape} element
  15. * @param {Point} position
  16. * @param {Function} getNextPosition
  17. * @return {Point}
  18. */
  19. export function findFreePosition(source: Shape, element: Shape, position: Point, getNextPosition: Function): Point {
  20. let connectedAtPosition;
  21. while ((connectedAtPosition = getConnectedAtPosition(source, position, element))) {
  22. position = getNextPosition(element, position, connectedAtPosition);
  23. }
  24. return position;
  25. }
  26. /**
  27. * Returns function that returns next position.
  28. *
  29. * @param {Object} nextPositionDirection
  30. * @param {Object} [nextPositionDirection.x]
  31. * @param {Object} [nextPositionDirection.y]
  32. * @returns {Function}
  33. */
  34. export function generateGetNextPosition(nextPositionDirection: { x?: any; y?: any }): Function {
  35. return function (element: Shape, previousPosition: Point, connectedAtPosition: Point) {
  36. const nextPosition: any = {
  37. x: previousPosition.x,
  38. y: previousPosition.y,
  39. };
  40. ['x', 'y'].forEach(axis => {
  41. const nextPositionDirectionForAxis = nextPositionDirection[axis];
  42. if (!nextPositionDirectionForAxis) return;
  43. const dimension = axis === 'x' ? 'width' : 'height';
  44. const margin = nextPositionDirectionForAxis.margin;
  45. const minDistance = nextPositionDirectionForAxis.minDistance;
  46. if (margin < 0) nextPosition[axis] = Math.min(connectedAtPosition[axis] + margin - element[dimension] / 2, previousPosition[axis] - minDistance + margin);
  47. else {
  48. nextPosition[axis] = Math.max(
  49. connectedAtPosition[axis] + connectedAtPosition[dimension] + margin + element[dimension] / 2,
  50. previousPosition[axis] + minDistance + margin,
  51. );
  52. }
  53. });
  54. return nextPosition;
  55. };
  56. }
  57. /**
  58. * Return target at given position, if defined.
  59. *
  60. * This takes connected elements from host and attachers
  61. * into account, too.
  62. */
  63. export function getConnectedAtPosition(source: Shape, position: Point, element: Shape): Shape | undefined {
  64. const bounds = {
  65. x: position.x - element.width / 2,
  66. y: position.y - element.height / 2,
  67. width: element.width,
  68. height: element.height,
  69. };
  70. const closure = getAutoPlaceClosure(source, element);
  71. return find(closure, (target: Shape) => {
  72. if (target === element) return false;
  73. const orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
  74. return orientation === 'intersect';
  75. });
  76. }
  77. /**
  78. * Compute optimal distance between source and target based on existing connections to and from source.
  79. * Assumes left-to-right and top-to-down modeling.
  80. *
  81. * @param {Shape} source
  82. * @param {Object} [hints]
  83. * @param {number} [hints.defaultDistance]
  84. * @param {string} [hints.direction]
  85. * @param {Function} [hints.filter]
  86. * @param {Function} [hints.getWeight]
  87. * @param {number} [hints.maxDistance]
  88. * @param {string} [hints.reference]
  89. * @return {number}
  90. */
  91. export function getConnectedDistance(
  92. source: Shape,
  93. hints?: {
  94. defaultDistance?: number;
  95. direction?: string;
  96. filter?: Function;
  97. getWeight?: Function;
  98. maxDistance?: number;
  99. reference?: string;
  100. },
  101. ): number {
  102. if (!hints) hints = {};
  103. function getDefaultWeight(connection: any): number {
  104. return connection.source === source ? 1 : -1;
  105. }
  106. const defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE;
  107. const direction = hints.direction || 'e';
  108. let filter = hints.filter;
  109. const getWeight = hints.getWeight || getDefaultWeight;
  110. const maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE;
  111. const reference = hints.reference || 'start';
  112. if (!filter) filter = noneFilter;
  113. function getDistance(a: any, b: any): number {
  114. if (direction === 'n') {
  115. if (reference === 'start') {
  116. return asTRBL(a).top - asTRBL(b).bottom;
  117. } else if (reference === 'center') {
  118. return asTRBL(a).top - getMid(b).y;
  119. } else {
  120. return asTRBL(a).top - asTRBL(b).top;
  121. }
  122. } else if (direction === 'w') {
  123. if (reference === 'start') return asTRBL(a).left - asTRBL(b).right;
  124. else if (reference === 'center') return asTRBL(a).left - getMid(b).x;
  125. else return asTRBL(a).left - asTRBL(b).left;
  126. } else if (direction === 's') {
  127. if (reference === 'start') return asTRBL(b).top - asTRBL(a).bottom;
  128. else if (reference === 'center') return getMid(b).y - asTRBL(a).bottom;
  129. else return asTRBL(b).bottom - asTRBL(a).bottom;
  130. } else {
  131. if (reference === 'start') return asTRBL(b).left - asTRBL(a).right;
  132. else if (reference === 'center') return getMid(b).x - asTRBL(a).right;
  133. else return asTRBL(b).right - asTRBL(a).right;
  134. }
  135. }
  136. const sourcesDistances = source.incoming.filter(filter).map((connection: any) => {
  137. const weight = getWeight(connection);
  138. const distance = weight < 5 ? getDistance(connection.source, source) : getDistance(source, connection.source);
  139. return {
  140. id: connection.source.id,
  141. distance: distance,
  142. weight: weight,
  143. };
  144. });
  145. const targetsDistances = source.outgoing.filter(filter).map((connection: any) => {
  146. const weight = getWeight(connection);
  147. const distance = weight > 5 ? getDistance(source, connection.target) : getDistance(connection.target, source);
  148. return {
  149. id: connection.target.id,
  150. distance: distance,
  151. weight: weight,
  152. };
  153. });
  154. const distances = sourcesDistances.concat(targetsDistances).reduce((accumulator, currentValue) => {
  155. accumulator[currentValue.id + '__weight_' + currentValue.weight] = currentValue;
  156. return accumulator;
  157. }, {});
  158. const distancesGrouped = reduce(
  159. distances,
  160. (accumulator, currentValue) => {
  161. const distance = currentValue.distance;
  162. const weight = currentValue.weight;
  163. if (distance < 0 || distance > maxDistance) return accumulator;
  164. if (!accumulator[String(distance)]) accumulator[String(distance)] = 0;
  165. accumulator[String(distance)] += 1 * weight;
  166. if (!accumulator.distance || accumulator[accumulator.distance] < accumulator[String(distance)]) accumulator.distance = distance;
  167. return accumulator;
  168. },
  169. {},
  170. );
  171. return distancesGrouped.distance || defaultDistance;
  172. }
  173. /**
  174. * Returns all connected elements around the given source.
  175. * This includes:
  176. * - connected elements
  177. * - host connected elements
  178. * - attachers connected elements
  179. * @param {Shape} source
  180. * @return {Array<Shape>}
  181. */
  182. function getAutoPlaceClosure(source: Shape, element: Shape): Array<Shape> {
  183. let allConnected = getConnected(source);
  184. if (source.host) allConnected = allConnected.concat(getConnected(source.host));
  185. if (source.attachers)
  186. allConnected = allConnected.concat(
  187. source.attachers.reduce((shapes: Array<Shape>, attacher: Shape) => {
  188. return shapes.concat(getConnected(attacher));
  189. }, []),
  190. );
  191. return allConnected;
  192. }
  193. function getConnected(element: Shape): Array<Shape> {
  194. return getTargets(element).concat(getSources(element));
  195. }
  196. function getSources(shape: Shape): Array<Shape> {
  197. return shape.incoming.map((connection: any) => {
  198. return connection.source;
  199. });
  200. }
  201. function getTargets(shape: Shape): Array<Shape> {
  202. return shape.outgoing.map((connection: any) => {
  203. return connection.target;
  204. });
  205. }
  206. function noneFilter(): boolean {
  207. return true;
  208. }