diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
308 lines (245 loc) • 7.26 kB
JavaScript
import {
isObject,
sortBy
} from 'min-dash';
import {
pointDistance,
pointsOnLine
} from '../util/Geometry';
import intersectPaths from 'path-intersection';
import { isConnection } from '../util/ModelUtil';
/**
* @typedef {import('../core/Types').ElementLike} Element
* @typedef {import('../core/Types').ConnectionLike} Connection
*
* @typedef {import('../util/Types').DirectionTRBL} DirectionTRBL
* @typedef {import('../util/Types').Intersection} Intersection
* @typedef {import('../util/Types').Point} Point
* @typedef {import('../util/Types').Rect} Rect
* @typedef {import('../util/Types').RectTRBL} RectTRBL
*/
/**
* @param {Rect} bounds
*
* @returns {Rect}
*/
export function roundBounds(bounds) {
return {
x: Math.round(bounds.x),
y: Math.round(bounds.y),
width: Math.round(bounds.width),
height: Math.round(bounds.height)
};
}
/**
* @param {Point} point
*
* @returns {Point}
*/
export function roundPoint(point) {
return {
x: Math.round(point.x),
y: Math.round(point.y)
};
}
/**
* Convert the given bounds to a { top, left, bottom, right } descriptor.
*
* @param {Point|Rect} bounds
*
* @return {RectTRBL}
*/
export function asTRBL(bounds) {
return {
top: bounds.y,
right: bounds.x + (bounds.width || 0),
bottom: bounds.y + (bounds.height || 0),
left: bounds.x
};
}
/**
* Convert a { top, left, bottom, right } to an objects bounds.
*
* @param {RectTRBL} trbl
*
* @return {Rect}
*/
export function asBounds(trbl) {
return {
x: trbl.left,
y: trbl.top,
width: trbl.right - trbl.left,
height: trbl.bottom - trbl.top
};
}
/**
* Get the mid of the given bounds or point.
*
* @param {Point|Rect} bounds
*
* @return {Point}
*/
export function getBoundsMid(bounds) {
return roundPoint({
x: bounds.x + (bounds.width || 0) / 2,
y: bounds.y + (bounds.height || 0) / 2
});
}
/**
* Get the mid of the given Connection.
*
* @param {Connection} connection
*
* @return {Point}
*/
export function getConnectionMid(connection) {
var waypoints = connection.waypoints;
// calculate total length and length of each segment
var parts = waypoints.reduce(function(parts, point, index) {
var lastPoint = waypoints[index - 1];
if (lastPoint) {
var lastPart = parts[parts.length - 1];
var startLength = lastPart && lastPart.endLength || 0;
var length = distance(lastPoint, point);
parts.push({
start: lastPoint,
end: point,
startLength: startLength,
endLength: startLength + length,
length: length
});
}
return parts;
}, []);
var totalLength = parts.reduce(function(length, part) {
return length + part.length;
}, 0);
// find which segement contains middle point
var midLength = totalLength / 2;
var i = 0;
var midSegment = parts[i];
while (midSegment.endLength < midLength) {
midSegment = parts[++i];
}
// calculate relative position on mid segment
var segmentProgress = (midLength - midSegment.startLength) / midSegment.length;
var midPoint = {
x: midSegment.start.x + (midSegment.end.x - midSegment.start.x) * segmentProgress,
y: midSegment.start.y + (midSegment.end.y - midSegment.start.y) * segmentProgress
};
return midPoint;
}
/**
* Get the mid of the given Element.
*
* @param {Element} element
*
* @return {Point}
*/
export function getMid(element) {
if (isConnection(element)) {
return getConnectionMid(element);
}
return getBoundsMid(element);
}
// orientation utils //////////////////////
/**
* Get orientation of the given rectangle with respect to
* the reference rectangle.
*
* A padding (positive or negative) may be passed to influence
* horizontal / vertical orientation and intersection.
*
* @param {Rect} rect
* @param {Rect} reference
* @param {Point|number} padding
*
* @return {DirectionTRBL|Intersection} the orientation; one of top, top-left, left, ..., bottom, right or intersect.
*/
export function getOrientation(rect, reference, padding) {
padding = padding || 0;
// make sure we can use an object, too
// for individual { x, y } padding
if (!isObject(padding)) {
padding = { x: padding, y: padding };
}
var rectOrientation = asTRBL(rect),
referenceOrientation = asTRBL(reference);
var top = rectOrientation.bottom + padding.y <= referenceOrientation.top,
right = rectOrientation.left - padding.x >= referenceOrientation.right,
bottom = rectOrientation.top - padding.y >= referenceOrientation.bottom,
left = rectOrientation.right + padding.x <= referenceOrientation.left;
var vertical = top ? 'top' : (bottom ? 'bottom' : null),
horizontal = left ? 'left' : (right ? 'right' : null);
if (horizontal && vertical) {
return vertical + '-' + horizontal;
} else {
return horizontal || vertical || 'intersect';
}
}
// intersection utils //////////////////////
/**
* Get intersection between an element and a line path.
*
* @param {string} elementPath
* @param {string} linePath
* @param {boolean} cropStart Whether to crop start or end.
*
* @return {Point}
*/
export function getElementLineIntersection(elementPath, linePath, cropStart) {
var intersections = getIntersections(elementPath, linePath);
// recognize intersections
// only one -> choose
// two close together -> choose first
// two or more distinct -> pull out appropriate one
// none -> ok (fallback to point itself)
if (intersections.length === 1) {
return roundPoint(intersections[0]);
} else if (intersections.length === 2 && pointDistance(intersections[0], intersections[1]) < 1) {
return roundPoint(intersections[0]);
} else if (intersections.length > 1) {
// sort by intersections based on connection segment +
// distance from start
intersections = sortBy(intersections, function(i) {
var distance = Math.floor(i.t2 * 100) || 1;
distance = 100 - distance;
distance = (distance < 10 ? '0' : '') + distance;
// create a sort string that makes sure we sort
// line segment ASC + line segment position DESC (for cropStart)
// line segment ASC + line segment position ASC (for cropEnd)
return i.segment2 + '#' + distance;
});
return roundPoint(intersections[cropStart ? 0 : intersections.length - 1]);
}
return null;
}
export function getIntersections(a, b) {
return intersectPaths(a, b);
}
export function filterRedundantWaypoints(waypoints) {
// alter copy of waypoints, not original
waypoints = waypoints.slice();
var idx = 0,
point,
previousPoint,
nextPoint;
while (waypoints[idx]) {
point = waypoints[idx];
previousPoint = waypoints[idx - 1];
nextPoint = waypoints[idx + 1];
if (pointDistance(point, nextPoint) === 0 ||
pointsOnLine(previousPoint, nextPoint, point)) {
// remove point, if overlapping with {nextPoint}
// or on line with {previousPoint} -> {point} -> {nextPoint}
waypoints.splice(idx, 1);
} else {
idx++;
}
}
return waypoints;
}
// helpers //////////////////////
function distance(a, b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}