| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- import {
- asTRBL,
- getOrientation,
- getMid
- } from '../../layout/LayoutUtil';
- import {
- find,
- reduce
- } from 'min-dash';
- // padding to detect element placement
- var PLACEMENT_DETECTION_PAD = 10;
- export var DEFAULT_DISTANCE = 50;
- var DEFAULT_MAX_DISTANCE = 250;
- /**
- * Get free position starting from given position.
- *
- * @param {djs.model.Shape} source
- * @param {djs.model.Shape} element
- * @param {Point} position
- * @param {Function} getNextPosition
- *
- * @return {Point}
- */
- export function findFreePosition(source, element, position, getNextPosition) {
- var 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) {
- return function(element, previousPosition, connectedAtPosition) {
- var nextPosition = {
- x: previousPosition.x,
- y: previousPosition.y
- };
- [ 'x', 'y' ].forEach(function(axis) {
- var nextPositionDirectionForAxis = nextPositionDirection[ axis ];
- if (!nextPositionDirectionForAxis) {
- return;
- }
- var dimension = axis === 'x' ? 'width' : 'height';
- var margin = nextPositionDirectionForAxis.margin,
- 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, position, element) {
- var bounds = {
- x: position.x - (element.width / 2),
- y: position.y - (element.height / 2),
- width: element.width,
- height: element.height
- };
- var closure = getAutoPlaceClosure(source, element);
- return find(closure, function(target) {
- if (target === element) {
- return false;
- }
- var 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 {djs.model.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, hints) {
- if (!hints) {
- hints = {};
- }
- // targets > sources by default
- function getDefaultWeight(connection) {
- return connection.source === source ? 1 : -1;
- }
- var defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE,
- direction = hints.direction || 'e',
- filter = hints.filter,
- getWeight = hints.getWeight || getDefaultWeight,
- maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE,
- reference = hints.reference || 'start';
- if (!filter) {
- filter = noneFilter;
- }
- function getDistance(a, b) {
- 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;
- }
- }
- }
- var sourcesDistances = source.incoming
- .filter(filter)
- .map(function(connection) {
- var weight = getWeight(connection);
- var distance = weight < 0
- ? getDistance(connection.source, source)
- : getDistance(source, connection.source);
- return {
- id: connection.source.id,
- distance: distance,
- weight: weight
- };
- });
- var targetsDistances = source.outgoing
- .filter(filter)
- .map(function(connection) {
- var weight = getWeight(connection);
- var distance = weight > 0
- ? getDistance(source, connection.target)
- : getDistance(connection.target, source);
- return {
- id: connection.target.id,
- distance: distance,
- weight: weight
- };
- });
- var distances = sourcesDistances.concat(targetsDistances).reduce(function(accumulator, currentValue) {
- accumulator[ currentValue.id + '__weight_' + currentValue.weight ] = currentValue;
- return accumulator;
- }, {});
- var distancesGrouped = reduce(distances, function(accumulator, currentValue) {
- var distance = currentValue.distance,
- 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 {djs.model.Shape} source
- *
- * @return {Array<djs.model.Shape>}
- */
- function getAutoPlaceClosure(source) {
- var allConnected = getConnected(source);
- if (source.host) {
- allConnected = allConnected.concat(getConnected(source.host));
- }
- if (source.attachers) {
- allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) {
- return shapes.concat(getConnected(attacher));
- }, []));
- }
- return allConnected;
- }
- function getConnected(element) {
- return getTargets(element).concat(getSources(element));
- }
- function getSources(shape) {
- return shape.incoming.map(function(connection) {
- return connection.source;
- });
- }
- function getTargets(shape) {
- return shape.outgoing.map(function(connection) {
- return connection.target;
- });
- }
- function noneFilter() {
- return true;
- }
|