// @ts-nocheck import { asTRBL, getOrientation, getMid } from 'diagram-js/lib/layout/LayoutUtil'; import { find, reduce } from 'min-dash'; import { Shape } from 'diagram-js/lib/model'; import { Point } from 'diagram-js/lib/util/Types'; import { DEFAULT_DISTANCE } from '../config/constants'; // padding to detect element placement const PLACEMENT_DETECTION_PAD = 10; const DEFAULT_MAX_DISTANCE = 350; /** * Get free position starting from given position. * * @param {Shape} source * @param {Shape} element * @param {Point} position * @param {Function} getNextPosition * @return {Point} */ export function findFreePosition(source: Shape, element: Shape, position: Point, getNextPosition: Function): Point { let connectedAtPosition; while ((connectedAtPosition = getConnectedAtPosition(source, position, element))) { position = getNextPosition(element, position, connectedAtPosition); } return position; } /** * Returns function that returns next position. * * @param {Object} nextPositionDirection * @param {Object} [nextPositionDirection.x] * @param {Object} [nextPositionDirection.y] * @returns {Function} */ export function generateGetNextPosition(nextPositionDirection: { x?: any; y?: any }): Function { return function (element: Shape, previousPosition: Point, connectedAtPosition: Point) { const nextPosition: any = { x: previousPosition.x, y: previousPosition.y, }; ['x', 'y'].forEach(axis => { const nextPositionDirectionForAxis = nextPositionDirection[axis]; if (!nextPositionDirectionForAxis) return; const dimension = axis === 'x' ? 'width' : 'height'; const margin = nextPositionDirectionForAxis.margin; const minDistance = nextPositionDirectionForAxis.minDistance; if (margin < 0) nextPosition[axis] = Math.min(connectedAtPosition[axis] + margin - element[dimension] / 2, previousPosition[axis] - minDistance + margin); else { nextPosition[axis] = Math.max( connectedAtPosition[axis] + connectedAtPosition[dimension] + margin + element[dimension] / 2, previousPosition[axis] + minDistance + margin, ); } }); return nextPosition; }; } /** * Return target at given position, if defined. * * This takes connected elements from host and attachers * into account, too. */ export function getConnectedAtPosition(source: Shape, position: Point, element: Shape): Shape | undefined { const bounds = { x: position.x - element.width / 2, y: position.y - element.height / 2, width: element.width, height: element.height, }; const closure = getAutoPlaceClosure(source, element); return find(closure, (target: Shape) => { if (target === element) return false; const orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD); return orientation === 'intersect'; }); } /** * Compute optimal distance between source and target based on existing connections to and from source. * Assumes left-to-right and top-to-down modeling. * * @param {Shape} source * @param {Object} [hints] * @param {number} [hints.defaultDistance] * @param {string} [hints.direction] * @param {Function} [hints.filter] * @param {Function} [hints.getWeight] * @param {number} [hints.maxDistance] * @param {string} [hints.reference] * @return {number} */ export function getConnectedDistance( source: Shape, hints?: { defaultDistance?: number; direction?: string; filter?: Function; getWeight?: Function; maxDistance?: number; reference?: string; }, ): number { if (!hints) hints = {}; function getDefaultWeight(connection: any): number { return connection.source === source ? 1 : -1; } const defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE; const direction = hints.direction || 'e'; let filter = hints.filter; const getWeight = hints.getWeight || getDefaultWeight; const maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE; const reference = hints.reference || 'start'; if (!filter) filter = noneFilter; function getDistance(a: any, b: any): number { if (direction === 'n') { if (reference === 'start') { return asTRBL(a).top - asTRBL(b).bottom; } else if (reference === 'center') { return asTRBL(a).top - getMid(b).y; } else { return asTRBL(a).top - asTRBL(b).top; } } else if (direction === 'w') { if (reference === 'start') return asTRBL(a).left - asTRBL(b).right; else if (reference === 'center') return asTRBL(a).left - getMid(b).x; else return asTRBL(a).left - asTRBL(b).left; } else if (direction === 's') { if (reference === 'start') return asTRBL(b).top - asTRBL(a).bottom; else if (reference === 'center') return getMid(b).y - asTRBL(a).bottom; else return asTRBL(b).bottom - asTRBL(a).bottom; } else { if (reference === 'start') return asTRBL(b).left - asTRBL(a).right; else if (reference === 'center') return getMid(b).x - asTRBL(a).right; else return asTRBL(b).right - asTRBL(a).right; } } const sourcesDistances = source.incoming.filter(filter).map((connection: any) => { const weight = getWeight(connection); const distance = weight < 5 ? getDistance(connection.source, source) : getDistance(source, connection.source); return { id: connection.source.id, distance: distance, weight: weight, }; }); const targetsDistances = source.outgoing.filter(filter).map((connection: any) => { const weight = getWeight(connection); const distance = weight > 5 ? getDistance(source, connection.target) : getDistance(connection.target, source); return { id: connection.target.id, distance: distance, weight: weight, }; }); const distances = sourcesDistances.concat(targetsDistances).reduce((accumulator, currentValue) => { accumulator[currentValue.id + '__weight_' + currentValue.weight] = currentValue; return accumulator; }, {}); const distancesGrouped = reduce( distances, (accumulator, currentValue) => { const distance = currentValue.distance; const weight = currentValue.weight; if (distance < 0 || distance > maxDistance) return accumulator; if (!accumulator[String(distance)]) accumulator[String(distance)] = 0; accumulator[String(distance)] += 1 * weight; if (!accumulator.distance || accumulator[accumulator.distance] < accumulator[String(distance)]) accumulator.distance = distance; return accumulator; }, {}, ); return distancesGrouped.distance || defaultDistance; } /** * Returns all connected elements around the given source. * This includes: * - connected elements * - host connected elements * - attachers connected elements * @param {Shape} source * @return {Array} */ function getAutoPlaceClosure(source: Shape, element: Shape): Array { let allConnected = getConnected(source); if (source.host) allConnected = allConnected.concat(getConnected(source.host)); if (source.attachers) allConnected = allConnected.concat( source.attachers.reduce((shapes: Array, attacher: Shape) => { return shapes.concat(getConnected(attacher)); }, []), ); return allConnected; } function getConnected(element: Shape): Array { return getTargets(element).concat(getSources(element)); } function getSources(shape: Shape): Array { return shape.incoming.map((connection: any) => { return connection.source; }); } function getTargets(shape: Shape): Array { return shape.outgoing.map((connection: any) => { return connection.target; }); } function noneFilter(): boolean { return true; }