@joint/core
Version:
JavaScript diagramming library
1,516 lines (1,267 loc) • 64 kB
JavaScript
import * as g from '../g/index.mjs';
const Directions = {
AUTO: 'auto',
LEFT: 'left',
RIGHT: 'right',
TOP: 'top',
BOTTOM: 'bottom',
ANCHOR_SIDE: 'anchor-side',
MAGNET_SIDE: 'magnet-side'
};
const DEFINED_DIRECTIONS = [Directions.LEFT, Directions.RIGHT, Directions.TOP, Directions.BOTTOM];
const OPPOSITE_DIRECTIONS = {
[Directions.LEFT]: Directions.RIGHT,
[Directions.RIGHT]: Directions.LEFT,
[Directions.TOP]: Directions.BOTTOM,
[Directions.BOTTOM]: Directions.TOP
};
const VERTICAL_DIRECTIONS = [Directions.TOP, Directions.BOTTOM];
const ANGLE_DIRECTION_MAP = {
0: Directions.RIGHT,
180: Directions.LEFT,
270: Directions.TOP,
90: Directions.BOTTOM
};
function getSegmentAngle(line) {
// TODO: the angle() method is general and therefore unnecessarily heavy for orthogonal links
return line.angle();
}
function simplifyPoints(points) {
// TODO: use own more efficient implementation (filter points that do not change direction).
// To simplify segments that are almost aligned (start and end points differ by e.g. 0.5px), use a threshold of 1.
return new g.Polyline(points).simplify({ threshold: 1 }).points;
}
function resolveSides(source, target) {
const { point: sourcePoint, x0: sx0, y0: sy0, view: sourceView, bbox: sourceBBox, direction: sourceDirection } = source;
const { point: targetPoint, x0: tx0, y0: ty0, view: targetView, bbox: targetBBox, direction: targetDirection } = target;
let sourceSide;
if (!sourceView) {
const sourceLinkAnchorBBox = new g.Rect(sx0, sy0, 0, 0);
sourceSide = DEFINED_DIRECTIONS.includes(sourceDirection) ? sourceDirection : sourceLinkAnchorBBox.sideNearestToPoint(targetPoint);
} else if (sourceView.model.isLink()) {
sourceSide = getDirectionForLinkConnection(targetPoint, sourcePoint, sourceView);
} else if (sourceDirection === Directions.ANCHOR_SIDE) {
sourceSide = sourceBBox.sideNearestToPoint(sourcePoint);
} else if (sourceDirection === Directions.MAGNET_SIDE) {
sourceSide = sourceView.model.getBBox().sideNearestToPoint(sourcePoint);
} else {
sourceSide = sourceDirection;
}
let targetSide;
if (!targetView) {
const targetLinkAnchorBBox = new g.Rect(tx0, ty0, 0, 0);
targetSide = DEFINED_DIRECTIONS.includes(targetDirection) ? targetDirection : targetLinkAnchorBBox.sideNearestToPoint(sourcePoint);
} else if (targetView.model.isLink()) {
targetSide = getDirectionForLinkConnection(sourcePoint, targetPoint, targetView);
} else if (targetDirection === Directions.ANCHOR_SIDE) {
targetSide = targetBBox.sideNearestToPoint(targetPoint);
} else if (targetDirection === Directions.MAGNET_SIDE) {
targetSide = targetView.model.getBBox().sideNearestToPoint(targetPoint);
} else {
targetSide = targetDirection;
}
return [sourceSide, targetSide];
}
function resolveForTopSourceSide(source, target, nextInLine) {
const { x0: sx0, y0: sy0, width, height, point: anchor, margin } = source;
const sx1 = sx0 + width;
const sy1 = sy0 + height;
const smx0 = sx0 - margin;
const smx1 = sx1 + margin;
const smy0 = sy0 - margin;
const { x: ax } = anchor;
const { x0: tx, y0: ty } = target;
if (tx === ax && ty < sy0) return Directions.BOTTOM;
if (tx < ax && ty < smy0) {
if (nextInLine.point.x === ax) return Directions.BOTTOM;
return Directions.RIGHT;
}
if (tx > ax && ty < smy0) {
if (nextInLine.point.x === ax) return Directions.BOTTOM;
return Directions.LEFT;
}
if (tx < smx0 && ty > smy0) return Directions.TOP;
if (tx > smx1 && ty > smy0) return Directions.TOP;
if (tx >= smx0 && tx <= ax && ty > sy1) {
if (nextInLine.point.x < tx) {
return Directions.RIGHT;
}
return Directions.LEFT;
}
if (tx <= smx1 && tx >= ax && ty > sy1) {
if (nextInLine.point.x < tx) {
return Directions.RIGHT;
}
return Directions.LEFT;
}
return Directions.BOTTOM;
}
function resolveForBottomSourceSide(source, target, nextInLine) {
const { x0: sx0, y0: sy0, width, height, point: anchor, margin } = source;
const sx1 = sx0 + width;
const sy1 = sy0 + height;
const smx0 = sx0 - margin;
const smx1 = sx1 + margin;
const smy1 = sy1 + margin;
const { x: ax } = anchor;
const { x0: tx, y0: ty } = target;
if (tx === ax && ty > sy1) return Directions.TOP;
if (tx < ax && ty > smy1) {
if (nextInLine.point.x === ax) return Directions.TOP;
return Directions.RIGHT;
}
if (tx > ax && ty > smy1) {
if (nextInLine.point.x === ax) return Directions.TOP;
return Directions.LEFT;
}
if (tx < smx0 && ty < smy1) return Directions.BOTTOM;
if (tx > smx1 && ty < smy1) return Directions.BOTTOM;
if (tx >= smx0 && tx <= ax && ty < sy0) {
if (nextInLine.point.x < tx) {
return Directions.RIGHT;
}
return Directions.LEFT;
}
if (tx <= smx1 && tx >= ax && ty < sy0) {
if (nextInLine.point.x < tx) {
return Directions.RIGHT;
}
return Directions.LEFT;
}
return Directions.TOP;
}
function resolveForLeftSourceSide(source, target, nextInLine) {
const { y0: sy0, x0: sx0, width, height, point: anchor, margin } = source;
const sx1 = sx0 + width;
const sy1 = sy0 + height;
const smx0 = sx0 - margin;
const smy0 = sy0 - margin;
const smy1 = sy1 + margin;
const { x: ax, y: ay } = anchor;
const { x0: tx, y0: ty } = target;
if (tx < ax && ty === ay) return Directions.RIGHT;
if (tx <= smx0 && ty < ay) return Directions.BOTTOM;
if (tx <= smx0 && ty > ay) return Directions.TOP;
if (tx >= smx0 && ty < smy0) return Directions.LEFT;
if (tx >= smx0 && ty > smy1) return Directions.LEFT;
if (tx > sx1 && ty >= smy0 && ty <= ay) {
if (nextInLine.point.y < ty) {
return Directions.BOTTOM;
}
return Directions.TOP;
}
if (tx > sx1 && ty <= smy1 && ty >= ay) {
if (nextInLine.point.y < ty) {
return Directions.BOTTOM;
}
return Directions.TOP;
}
return Directions.RIGHT;
}
function resolveForRightSourceSide(source, target, nextInLine) {
const { y0: sy0, x0: sx0, width, height, point: anchor, margin } = source;
const sx1 = sx0 + width;
const sy1 = sy0 + height;
const smx1 = sx1 + margin;
const smy0 = sy0 - margin;
const smy1 = sy1 + margin;
const { x: ax, y: ay } = anchor;
const { x0: tx, y0: ty } = target;
if (tx > ax && ty === ay) return Directions.LEFT;
if (tx >= smx1 && ty < ay) return Directions.BOTTOM;
if (tx >= smx1 && ty > ay) return Directions.TOP;
if (tx <= smx1 && ty < smy0) return Directions.RIGHT;
if (tx <= smx1 && ty > smy1) return Directions.RIGHT;
if (tx < sx0 && ty >= smy0 && ty <= ay) {
if (nextInLine.point.y < ty) {
return Directions.BOTTOM;
}
return Directions.TOP;
}
if (tx < sx0 && ty <= smy1 && ty >= ay) {
if (nextInLine.point.y < ty) {
return Directions.BOTTOM;
}
return Directions.TOP;
}
return Directions.LEFT;
}
function resolveInitialDirection(source, target, nextInLine) {
const [sourceSide] = resolveSides(source, target);
switch (sourceSide) {
case Directions.TOP:
return resolveForTopSourceSide(source, target, nextInLine);
case Directions.RIGHT:
return resolveForRightSourceSide(source, target, nextInLine);
case Directions.BOTTOM:
return resolveForBottomSourceSide(source, target, nextInLine);
case Directions.LEFT:
return resolveForLeftSourceSide(source, target, nextInLine);
}
}
function getDirectionForLinkConnection(linkOrigin, connectionPoint, linkView) {
const tangent = linkView.getTangentAtLength(linkView.getClosestPointLength(connectionPoint));
const roundedAngle = Math.round(getSegmentAngle(tangent) / 90) * 90;
if (roundedAngle % 180 === 0 && linkOrigin.y === connectionPoint.y) {
return linkOrigin.x < connectionPoint.x ? Directions.LEFT : Directions.RIGHT;
} else if (linkOrigin.x === connectionPoint.x) {
return linkOrigin.y < connectionPoint.y ? Directions.TOP : Directions.BOTTOM;
}
switch (roundedAngle) {
case 0:
case 180:
case 360:
return linkOrigin.y < connectionPoint.y ? Directions.TOP : Directions.BOTTOM;
case 90:
case 270:
return linkOrigin.x < connectionPoint.x ? Directions.LEFT : Directions.RIGHT;
}
}
function pointDataFromAnchor(view, point, bbox, direction, isPort, fallBackAnchor, margin) {
if (direction === Directions.AUTO) {
direction = isPort ? Directions.MAGNET_SIDE : Directions.ANCHOR_SIDE;
}
const isElement = view && view.model.isElement();
const {
x: x0,
y: y0,
width = 0,
height = 0
} = isElement ? g.Rect.fromRectUnion(bbox, view.model.getBBox()) : fallBackAnchor;
return {
point,
x0,
y0,
view,
bbox,
width,
height,
direction,
margin: isElement ? margin : 0
};
}
function pointDataFromVertex({ x, y }) {
const point = new g.Point(x, y);
return {
point,
x0: point.x,
y0: point.y,
view: null,
bbox: new g.Rect(x, y, 0, 0),
width: 0,
height: 0,
direction: null,
margin: 0
};
}
function getOutsidePoint(side, pointData, margin) {
const outsidePoint = pointData.point.clone();
const { x0, y0, width, height } = pointData;
switch (side) {
case 'left':
outsidePoint.x = x0 - margin;
break;
case 'right':
outsidePoint.x = x0 + width + margin;
break;
case 'top':
outsidePoint.y = y0 - margin;
break;
case 'bottom':
outsidePoint.y = y0 + height + margin;
break;
}
return outsidePoint;
}
function createLoop(from, to, { dx = 0, dy = 0 }) {
const p1 = { x: from.point.x + dx, y: from.point.y + dy };
const p2 = { x: to.point.x + dx, y: to.point.y + dy };
return [from.point, p1, p2, to.point];
}
function loopSegment(from, to, connectionSegmentAngle, margin) {
// Find out the loop coordinates.
const angle = g.normalizeAngle(connectionSegmentAngle - 90);
let dx = 0;
let dy = 0;
if (angle === 90) {
dy = -margin;
} else if (angle === 180) {
dx = -margin;
} else if (angle === 270) {
dy = margin;
} else if (angle === 0) {
dx = margin;
}
const loopRoute = createLoop(from, to, { dx, dy });
const secondCreatedPoint = loopRoute[2];
const loopEndSegment = new g.Line(to.point, secondCreatedPoint);
// The direction in which the loop should continue.
const continueDirection = ANGLE_DIRECTION_MAP[getSegmentAngle(loopEndSegment)];
return {
loopRoute,
continueDirection
};
}
// Calculates the distances along the horizontal axis for the left and right route.
function getHorizontalDistance(source, target) {
const { x0: sx0, x1: sx1, outsidePoint: sourcePoint } = source;
const { x0: tx0, x1: tx1, outsidePoint: targetPoint } = target;
// Furthest left boundary
let leftBoundary = Math.min(sx0, tx0);
// Furthest right boundary
let rightBoundary = Math.max(sx1, tx1);
// If the source and target elements are on the same side, we need to figure out what shape defines the boundary.
if (source.direction === target.direction) {
const aboveShape = source.y0 < target.y0 ? source : target;
const belowShape = aboveShape === source ? target : source;
// The source and target anchors are on the top => then the `aboveShape` defines the boundary.
// The source and target anchors are on the bottom => then the `belowShape` defines the boundary.
const boundaryDefiningShape = source.direction === Directions.TOP ? aboveShape : belowShape;
leftBoundary = boundaryDefiningShape.x0;
rightBoundary = boundaryDefiningShape.x1;
}
const { x: sox } = sourcePoint;
const { x: tox } = targetPoint;
// Calculate the distances for the left route
const leftDistance1 = Math.abs(sox - leftBoundary);
const leftDistance2 = Math.abs(tox - leftBoundary);
const leftD = leftDistance1 + leftDistance2;
// Calculate the distances for the right route
const rightDistance1 = Math.abs(sox - rightBoundary);
const rightDistance2 = Math.abs(tox - rightBoundary);
const rightD = rightDistance1 + rightDistance2;
return [leftD, rightD];
}
// Calculates the distances along the vertical axis for the top and bottom route.
function getVerticalDistance(source, target) {
const { y0: sy0, y1: sy1, outsidePoint: sourcePoint } = source;
const { y0: ty0, y1: ty1, outsidePoint: targetPoint } = target;
// Furthest top boundary
let topBoundary = Math.min(sy0, ty0);
// Furthest bottom boundary
let bottomBoundary = Math.max(sy1, ty1);
// If the source and target elements are on the same side, we need to figure out what shape defines the boundary.
if (source.direction === target.direction) {
const leftShape = source.x0 < target.x0 ? source : target;
const rightShape = leftShape === source ? target : source;
// The source and target anchors are on the left => then the `leftShape` defines the boundary.
// The source and target anchors are on the right => then the `rightShape` defines the boundary.
const boundaryDefiningShape = source.direction === Directions.LEFT ? leftShape : rightShape;
topBoundary = boundaryDefiningShape.y0;
bottomBoundary = boundaryDefiningShape.y1;
}
const { y: soy } = sourcePoint;
const { y: toy } = targetPoint;
// Calculate the distances for the top route
const topDistance1 = Math.abs(soy - topBoundary);
const topDistance2 = Math.abs(toy - topBoundary);
const topD = topDistance1 + topDistance2;
// Calculate the distances for the bottom route
const bottomDistance1 = Math.abs(soy - bottomBoundary);
const bottomDistance2 = Math.abs(toy - bottomBoundary);
const bottomD = bottomDistance1 + bottomDistance2;
return [topD, bottomD];
}
// Inflate bbox in 3 directions depending on the direction of the anchor
// don't inflate in the opposite direction of the anchor
function moveAndExpandBBox(bbox, direction, margin) {
switch (direction) {
case Directions.LEFT:
bbox.inflate(0, margin).moveAndExpand({ x: -margin, width: margin });
break;
case Directions.RIGHT:
bbox.inflate(0, margin).moveAndExpand({ width: margin });
break;
case Directions.TOP:
bbox.inflate(margin, 0).moveAndExpand({ y: -margin, height: margin });
break;
case Directions.BOTTOM:
bbox.inflate(margin, 0).moveAndExpand({ height: margin });
break;
}
return bbox;
}
function routeBetweenPoints(source, target, opt = {}) {
const { point: sourcePoint, x0: sx0, y0: sy0, width: sourceWidth, height: sourceHeight, margin: sourceMargin } = source;
const { point: targetPoint, x0: tx0, y0: ty0, width: targetWidth, height: targetHeight, margin: targetMargin } = target;
const { targetInSourceBBox = false } = opt;
const tx1 = tx0 + targetWidth;
const ty1 = ty0 + targetHeight;
const sx1 = sx0 + sourceWidth;
const sy1 = sy0 + sourceHeight;
// Key coordinates including the margin
const smx0 = sx0 - sourceMargin;
const smx1 = sx1 + sourceMargin;
const smy0 = sy0 - sourceMargin;
const smy1 = sy1 + sourceMargin;
const tmx0 = tx0 - targetMargin;
const tmx1 = tx1 + targetMargin;
const tmy0 = ty0 - targetMargin;
const tmy1 = ty1 + targetMargin;
const [sourceSide, targetSide] = resolveSides(source, target);
const sourceOutsidePoint = getOutsidePoint(sourceSide, { point: sourcePoint, x0: sx0, y0: sy0, width: sourceWidth, height: sourceHeight }, sourceMargin);
const targetOutsidePoint = getOutsidePoint(targetSide, { point: targetPoint, x0: tx0, y0: ty0, width: targetWidth, height: targetHeight }, targetMargin);
const { x: sox, y: soy } = sourceOutsidePoint;
const { x: tox, y: toy } = targetOutsidePoint;
const tcx = (tx0 + tx1) / 2;
const tcy = (ty0 + ty1) / 2;
const scx = (sx0 + sx1) / 2;
const scy = (sy0 + sy1) / 2;
const middleOfVerticalSides = (scx < tcx ? (sx1 + tx0) : (tx1 + sx0)) / 2;
const middleOfHorizontalSides = (scy < tcy ? (sy1 + ty0) : (ty1 + sy0)) / 2;
const sourceBBox = new g.Rect(sx0, sy0, sourceWidth, sourceHeight);
const targetBBox = new g.Rect(tx0, ty0, targetWidth, targetHeight);
const inflatedSourceBBox = sourceBBox.clone().inflate(sourceMargin);
const inflatedTargetBBox = targetBBox.clone().inflate(targetMargin);
const sourceForDistance = Object.assign({}, source, { x1: sx1, y1: sy1, outsidePoint: sourceOutsidePoint, direction: sourceSide });
const targetForDistance = Object.assign({}, target, { x1: tx1, y1: ty1, outsidePoint: targetOutsidePoint, direction: targetSide });
// Distances used to determine the shortest route along the connections on horizontal sides for
// bottom => bottom
// top => bottom
// bottom => top
// top => top
const [leftD, rightD] = getHorizontalDistance(sourceForDistance, targetForDistance);
// Distances used to determine the shortest route along the connection on vertical sides for
// left => left
// left => right
// right => right
// right => left
const [topD, bottomD] = getVerticalDistance(sourceForDistance, targetForDistance);
// All possible combinations of source and target sides
if (sourceSide === 'left' && targetSide === 'right') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint);
const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint);
// Use S-shaped connection
if (isPointInsideSource || isPointInsideTarget) {
const middleOfAnchors = (soy + toy) / 2;
return [
{ x: sox, y: soy },
{ x: sox, y: middleOfAnchors },
{ x: tox, y: middleOfAnchors },
{ x: tox, y: toy }
];
}
if (smx0 < tox) {
let y = middleOfHorizontalSides;
let x1 = sox;
let x2 = tox;
const isUpwardsShorter = topD < bottomD;
// If the source and target elements overlap, we need to make sure the connection
// goes around the target element.
if ((y >= smy0 && y <= smy1) || (y >= tmy0 && y <= tmy1)) {
if (smy1 >= tmy0 && isUpwardsShorter) {
y = Math.min(tmy0, smy0);
} else if (smy0 <= tmy1 && !isUpwardsShorter) {
y = Math.max(tmy1, smy1);
}
// This handles the case when the source and target elements overlap as well as
// the case when the source is to the left of the target element.
x1 = Math.min(sox, tmx0);
x2 = Math.max(tox, smx1);
// This is an edge case when the source and target intersect and
if ((isUpwardsShorter && soy < ty0) || (!isUpwardsShorter && soy > ty1)) {
// the path should no longer rely on minimal x boundary in `x1`
x1 = sox;
} else if ((isUpwardsShorter && toy < sy0) || (!isUpwardsShorter && toy > sy1)) {
// the path should no longer rely on maximal x boundary in `x2`
x2 = tox;
}
}
return [
{ x: x1, y: soy },
{ x: x1, y },
{ x: x2, y },
{ x: x2, y: toy }
];
}
const x = (sox + tox) / 2;
return [
{ x, y: soy },
{ x, y: toy },
];
} else if (sourceSide === 'right' && targetSide === 'left') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint);
const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint);
// Use S-shaped connection
if (isPointInsideSource || isPointInsideTarget) {
const middleOfAnchors = (soy + toy) / 2;
return [
{ x: sox, y: soy },
{ x: sox, y: middleOfAnchors },
{ x: tox, y: middleOfAnchors },
{ x: tox, y: toy }
];
}
if (smx1 > tox) {
let y = middleOfHorizontalSides;
let x1 = sox;
let x2 = tox;
const isUpwardsShorter = topD < bottomD;
// If the source and target elements overlap, we need to make sure the connection
// goes around the target element.
if ((y >= smy0 && y <= smy1) || (y >= tmy0 && y <= tmy1)) {
if (smy1 >= tmy0 && isUpwardsShorter) {
y = Math.min(tmy0, smy0);
} else if (smy0 <= tmy1 && !isUpwardsShorter) {
y = Math.max(tmy1, smy1);
}
// This handles the case when the source and target elements overlap as well as
// the case when the source is to the left of the target element.
x1 = Math.max(sox, tmx1);
x2 = Math.min(tox, smx0);
// This is an edge case when the source and target intersect and
if ((isUpwardsShorter && soy < ty0) || (!isUpwardsShorter && soy > ty1)) {
// the path should no longer rely on maximal x boundary in `x1`
x1 = sox;
} else if ((isUpwardsShorter && toy < sy0) || (!isUpwardsShorter && toy > sy1)) {
// the path should no longer rely on minimal x boundary in `x2`
x2 = tox;
}
}
return [
{ x: x1, y: soy },
{ x: x1, y },
{ x: x2, y },
{ x: x2, y: toy }
];
}
const x = (sox + tox) / 2;
return [
{ x, y: soy },
{ x, y: toy }
];
} else if (sourceSide === 'top' && targetSide === 'bottom') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint);
const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint);
// Use S-shaped connection
if (isPointInsideSource || isPointInsideTarget) {
const middleOfAnchors = (sox + tox) / 2;
return [
{ x: sox, y: soy },
{ x: middleOfAnchors, y: soy },
{ x: middleOfAnchors, y: toy },
{ x: tox, y: toy }
];
}
if (smy0 < toy) {
let x = middleOfVerticalSides;
let y1 = soy;
let y2 = toy;
const isLeftShorter = leftD < rightD;
// If the source and target elements overlap, we need to make sure the connection
// goes around the target element.
if ((x >= smx0 && x <= smx1) || (x >= tmx0 && x <= tmx1)) {
if (smx1 >= tmx0 && isLeftShorter) {
x = Math.min(tmx0, smx0);
} else if (smx0 <= tmx1 && !isLeftShorter) {
x = Math.max(tmx1, smx1);
}
// This handles the case when the source and target elements overlap as well as
// the case when the source is to the left of the target element.
y1 = Math.min(soy, tmy0);
y2 = Math.max(toy, smy1);
// This is an edge case when the source and target intersect and
if ((isLeftShorter && sox < tx0) || (!isLeftShorter && sox > tx1)) {
// the path should no longer rely on minimal y boundary in `y1`
y1 = soy;
} else if ((isLeftShorter && tox < sx0) || (!isLeftShorter && tox > sx1)) {
// the path should no longer rely on maximal y boundary in `y2`
y2 = toy;
}
}
return [
{ x: sox, y: y1 },
{ x, y: y1 },
{ x, y: y2 },
{ x: tox, y: y2 }
];
}
const y = (soy + toy) / 2;
return [
{ x: sox, y },
{ x: tox, y }
];
} else if (sourceSide === 'bottom' && targetSide === 'top') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint);
const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint);
// Use S-shaped connection
if (isPointInsideSource || isPointInsideTarget) {
const middleOfAnchors = (sox + tox) / 2;
return [
{ x: sox, y: soy },
{ x: middleOfAnchors, y: soy },
{ x: middleOfAnchors, y: toy },
{ x: tox, y: toy }
];
}
if (smy1 > toy) {
let x = middleOfVerticalSides;
let y1 = soy;
let y2 = toy;
const isLeftShorter = leftD < rightD;
// If the source and target elements overlap, we need to make sure the connection
// goes around the target element.
if ((x >= smx0 && x <= smx1) || (x >= tmx0 && x <= tmx1)) {
if (smx1 >= tmx0 && isLeftShorter) {
x = Math.min(tmx0, smx0);
} else if (smx0 <= tmx1 && !isLeftShorter) {
x = Math.max(tmx1, smx1);
}
// This handles the case when the source and target elements overlap as well as
// the case when the source is to the left of the target element.
y1 = Math.max(soy, tmy1);
y2 = Math.min(toy, smy0);
// This is an edge case when the source and target intersect and
if ((isLeftShorter && sox < tx0) || (!isLeftShorter && sox > tx1)) {
// the path should no longer rely on maximal y boundary in `y1`
y1 = soy;
} else if ((isLeftShorter && tox < sx0) || (!isLeftShorter && tox > sx1)) {
// the path should no longer rely on minimal y boundary in `y2`
y2 = toy;
}
}
return [
{ x: sox, y: y1 },
{ x, y: y1 },
{ x, y: y2 },
{ x: tox, y: y2 }
];
}
const y = (soy + toy) / 2;
return [
{ x: sox, y },
{ x: tox, y }
];
} else if (sourceSide === 'top' && targetSide === 'top') {
const useUShapeConnection =
targetInSourceBBox ||
g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) ||
(soy <= ty0 && (inflatedSourceBBox.bottomRight().x <= tox || inflatedSourceBBox.bottomLeft().x >= tox)) ||
(soy >= ty0 && (inflatedTargetBBox.bottomRight().x <= sox || inflatedTargetBBox.bottomLeft().x >= sox));
// U-shape connection is a straight line if `sox` and `tox` are the same
if (useUShapeConnection && sox !== tox) {
return [
{ x: sox, y: Math.min(soy, toy) },
{ x: tox, y: Math.min(soy, toy) }
];
}
let x;
let y1 = Math.min((sy1 + ty0) / 2, toy);
let y2 = Math.min((sy0 + ty1) / 2, soy);
if (toy < soy) {
// Use the shortest path along the connections on horizontal sides
if (rightD > leftD) {
x = Math.min(sox, tmx0);
} else {
x = Math.max(sox, tmx1);
}
} else {
if (rightD > leftD) {
x = Math.min(tox, smx0);
} else {
x = Math.max(tox, smx1);
}
}
return [
{ x: sox, y: y2 },
{ x, y: y2 },
{ x, y: y1 },
{ x: tox, y: y1 }
];
} else if (sourceSide === 'bottom' && targetSide === 'bottom') {
const useUShapeConnection =
targetInSourceBBox ||
g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) ||
(soy >= toy && (inflatedSourceBBox.topRight().x <= tox || inflatedSourceBBox.topLeft().x >= tox)) ||
(soy <= toy && (inflatedTargetBBox.topRight().x <= sox || inflatedTargetBBox.topLeft().x >= sox));
// U-shape connection is a straight line if `sox` and `tox` are the same
if (useUShapeConnection && sox !== tox) {
return [
{ x: sox, y: Math.max(soy, toy) },
{ x: tox, y: Math.max(soy, toy) }
];
}
let x;
let y1 = Math.max((sy0 + ty1) / 2, toy);
let y2 = Math.max((sy1 + ty0) / 2, soy);
if (toy > soy) {
// Use the shortest path along the connections on horizontal sides
if (rightD > leftD) {
x = Math.min(sox, tmx0);
} else {
x = Math.max(sox, tmx1);
}
} else {
if (rightD > leftD) {
x = Math.min(tox, smx0);
} else {
x = Math.max(tox, smx1);
}
}
return [
{ x: sox, y: y2 },
{ x, y: y2 },
{ x, y: y1 },
{ x: tox, y: y1 }
];
} else if (sourceSide === 'left' && targetSide === 'left') {
const useUShapeConnection =
targetInSourceBBox ||
g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) ||
(sox <= tox && (inflatedSourceBBox.bottomRight().y <= toy || inflatedSourceBBox.topRight().y >= toy)) ||
(sox >= tox && (inflatedTargetBBox.bottomRight().y <= soy || inflatedTargetBBox.topRight().y >= soy));
// U-shape connection is a straight line if `soy` and `toy` are the same
if (useUShapeConnection && soy !== toy) {
return [
{ x: Math.min(sox, tox), y: soy },
{ x: Math.min(sox, tox), y: toy }
];
}
let y;
let x1 = Math.min((sx1 + tx0) / 2, tox);
let x2 = Math.min((sx0 + tx1) / 2, sox);
if (tox > sox) {
if (topD <= bottomD) {
y = Math.min(smy0, toy);
} else {
y = Math.max(smy1, toy);
}
} else {
if (topD <= bottomD) {
y = Math.min(tmy0, soy);
} else {
y = Math.max(tmy1, soy);
}
}
return [
{ x: x2, y: soy },
{ x: x2, y },
{ x: x1, y },
{ x: x1, y: toy }
];
} else if (sourceSide === 'right' && targetSide === 'right') {
const useUShapeConnection =
targetInSourceBBox ||
g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) ||
(sox >= tox && (inflatedSourceBBox.bottomLeft().y <= toy || inflatedSourceBBox.topLeft().y >= toy)) ||
(sox <= tox && (inflatedTargetBBox.bottomLeft().y <= soy || inflatedTargetBBox.topLeft().y >= soy));
// U-shape connection is a straight line if `soy` and `toy` are the same
if (useUShapeConnection && soy !== toy) {
return [
{ x: Math.max(sox, tox), y: soy },
{ x: Math.max(sox, tox), y: toy }
];
}
let y;
let x1 = Math.max((sx0 + tx1) / 2, tox);
let x2 = Math.max((sx1 + tx0) / 2, sox);
if (tox <= sox) {
if (topD <= bottomD) {
y = Math.min(smy0, toy);
} else {
y = Math.max(smy1, toy);
}
} else {
if (topD <= bottomD) {
y = Math.min(tmy0, soy);
} else {
y = Math.max(tmy1, soy);
}
}
return [
{ x: x2, y: soy },
{ x: x2, y },
{ x: x1, y },
{ x: x1, y: toy }
];
} else if (sourceSide === 'top' && targetSide === 'right') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (sox <= tmx1) {
const x = Math.max(sox + sourceMargin, tox);
const y = Math.min(smy0, tmy0);
// Target anchor is on the right side of the source anchor
return [
{ x: sox, y },
{ x: x, y },
{ x: x, y: toy }
];
}
// Target anchor is on the left side of the source anchor
// Subtract the `sourceMargin` since the source anchor is on the right side of the target anchor
const anchorMiddleX = (sox - sourceMargin + tox) / 2;
return [
{ x: sox, y: soy },
{ x: anchorMiddleX, y: soy },
{ x: anchorMiddleX, y: toy }
];
}
if (smy0 > toy) {
if (sox < tox) {
let y = tmy0;
if (tmy1 <= smy0 && tmx1 >= sox) {
y = middleOfHorizontalSides;
}
return [
{ x: sox, y },
{ x: tox, y },
{ x: tox, y: toy }
];
}
return [{ x: sox, y: toy }];
}
const x = Math.max(middleOfVerticalSides, tmx1);
if (sox > tox && sy1 >= toy) {
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
}
if (x > smx0 && soy < ty1) {
const y = Math.min(smy0, tmy0);
const x = Math.max(smx1, tmx1);
return [
{ x: sox, y },
{ x, y },
{ x, y: toy }
];
}
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
} else if (sourceSide === 'top' && targetSide === 'left') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (sox >= tmx0) {
const x = Math.min(sox - sourceMargin, tox);
const y = Math.min(smy0, tmy0);
// Target anchor is on the left side of the source anchor
return [
{ x: sox, y },
{ x: x, y },
{ x: x, y: toy }
];
}
// Target anchor is on the right side of the source anchor
// Add the `sourceMargin` since the source anchor is on the left side of the target anchor
const anchorMiddleX = (sox + sourceMargin + tox) / 2;
return [
{ x: sox, y: soy },
{ x: anchorMiddleX, y: soy },
{ x: anchorMiddleX, y: toy }
];
}
if (smy0 > toy) {
if (sox > tox) {
let y = tmy0;
if (tmy1 <= smy0 && tmx0 <= sox) {
y = middleOfHorizontalSides;
}
return [
{ x: sox, y },
{ x: tox, y },
{ x: tox, y: toy }
];
}
return [{ x: sox, y: toy }];
}
const x = Math.min(tmx0, middleOfVerticalSides);
if (sox < tox && sy1 >= toy) {
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }];
}
if (x < smx1 && soy < ty1) {
const y = Math.min(smy0, tmy0);
const x = Math.min(smx0, tmx0);
return [
{ x: sox, y },
{ x, y },
{ x, y: toy }
];
}
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
} else if (sourceSide === 'bottom' && targetSide === 'right') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (sox <= tmx1) {
const x = Math.max(sox + sourceMargin, tox);
const y = Math.max(smy1, tmy1);
// Target anchor is on the right side of the source anchor
return [
{ x: sox, y },
{ x, y },
{ x, y: toy }
];
}
// Target anchor is on the left side of the source anchor
// Subtract the `sourceMargin` since the source anchor is on the right side of the target anchor
const anchorMiddleX = (sox - sourceMargin + tox) / 2;
return [
{ x: sox, y: soy },
{ x: anchorMiddleX, y: soy },
{ x: anchorMiddleX, y: toy }
];
}
if (smy1 < toy) {
if (sox < tox) {
let y = tmy1;
if (tmy0 >= smy1 && tmx1 >= sox) {
y = middleOfHorizontalSides;
}
return [
{ x: sox, y },
{ x: tox, y },
{ x: tox, y: toy }
];
}
return [{ x: sox, y: toy }];
}
const x = Math.max(middleOfVerticalSides, tmx1);
if (sox > tox && sy0 <= toy) {
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
}
if (x > smx0 && soy > ty0) {
const y = Math.max(smy1, tmy1);
const x = Math.max(smx1, tmx1);
return [
{ x: sox, y },
{ x, y },
{ x, y: toy }
];
}
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
} else if (sourceSide === 'bottom' && targetSide === 'left') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (sox >= tmx0) {
const x = Math.min(sox - sourceMargin, tox);
const y = Math.max(smy1, tmy1);
// Target anchor is on the left side of the source anchor
return [
{ x: sox, y },
{ x, y },
{ x, y: toy }
];
}
// Target anchor is on the right side of the source anchor
// Add the `sourceMargin` since the source anchor is on the left side of the target anchor
const anchorMiddleX = (sox + sourceMargin + tox) / 2;
return [
{ x: sox, y: soy },
{ x: anchorMiddleX, y: soy },
{ x: anchorMiddleX, y: toy }
];
}
if (smy1 < toy) {
if (sox > tox) {
let y = tmy1;
if (tmy0 >= smy1 && tmx0 <= sox) {
y = middleOfHorizontalSides;
}
return [
{ x: sox, y },
{ x: tox, y },
{ x: tox, y: toy }
];
}
return [{ x: sox, y: toy }];
}
const x = Math.min(tmx0, middleOfVerticalSides);
if (sox < tox && sy0 <= toy) {
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
}
if (x < smx1 && soy > ty0) {
const y = Math.max(smy1, tmy1);
const x = Math.min(smx0, tmx0);
return [
{ x: sox, y },
{ x, y },
{ x, y: toy }
];
}
return [
{ x: sox, y: soy },
{ x, y: soy },
{ x, y: toy }
];
} else if (sourceSide === 'left' && targetSide === 'bottom') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (soy <= tmy1) {
const x = Math.min(smx0, tmx0);
const y = Math.max(soy + sourceMargin, toy);
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
// Target anchor is above the source anchor
const anchorMiddleY = (soy - sourceMargin + toy) / 2;
return [
{ x: sox, y: soy },
{ x: sox, y: anchorMiddleY },
{ x: tox, y: anchorMiddleY }
];
}
if (smx0 > tox) {
if (soy < toy) {
let x = tmx0;
if (tmx1 <= smx0 && tmy1 >= soy) {
x = middleOfVerticalSides;
}
return [
{ x, y: soy },
{ x, y: toy },
{ x: tox, y: toy }
];
}
return [{ x: tox, y: soy }];
}
const y = Math.max(tmy1, middleOfHorizontalSides);
if (soy > toy && sx1 >= tox) {
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }
];
}
if (y > smy0 && sox < tx1) {
const x = Math.min(smx0, tmx0);
const y = Math.max(smy1, tmy1);
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }
];
} else if (sourceSide === 'left' && targetSide === 'top') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (soy >= tmy0) {
const y = Math.min(soy - sourceMargin, toy);
const x = Math.min(smx0, tmx0);
// Target anchor is on the top side of the source anchor
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
// Target anchor is below the source anchor
// Add the `sourceMargin` since the source anchor is above the target anchor
const anchorMiddleY = (soy + sourceMargin + toy) / 2;
return [
{ x: sox, y: soy },
{ x: sox, y: anchorMiddleY },
{ x: tox, y: anchorMiddleY }
];
}
if (smx0 > tox) {
if (soy > toy) {
let x = tmx0;
if (tmx1 <= smx0 && tmy0 <= soy) {
x = middleOfVerticalSides;
}
return [
{ x, y: soy },
{ x, y: toy },
{ x: tox, y: toy }
];
}
return [{ x: tox, y: soy }];
}
const y = Math.min(tmy0, middleOfHorizontalSides);
if (soy < toy && sx1 >= tox) {
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }];
}
if (y < smy1 && sox < tx1) {
const x = Math.min(smx0, tmx0);
const y = Math.min(smy0, tmy0);
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }
];
} else if (sourceSide === 'right' && targetSide === 'top') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (soy >= tmy0) {
const x = Math.max(smx1, tmx1);
const y = Math.min(soy - sourceMargin, toy);
// Target anchor is on the top side of the source anchor
return [
{ x, y: soy },
{ x, y }, // Path adjustment for right side start
{ x: tox, y }
];
}
// Target anchor is below the source anchor
// Adjust sourceMargin calculation since the source anchor is now on the right
const anchorMiddleY = (soy + sourceMargin + toy) / 2;
return [
{ x: sox, y: soy },
{ x: sox, y: anchorMiddleY },
{ x: tox, y: anchorMiddleY }
];
}
if (smx1 < tox) {
if (soy > toy) {
let x = tmx1;
if (tmx0 >= smx1 && tmy0 <= soy) {
x = middleOfVerticalSides;
}
return [
{ x, y: soy },
{ x, y: toy },
{ x: tox, y: toy }
];
}
return [{ x: tox, y: soy }];
}
const y = Math.min(tmy0, middleOfHorizontalSides);
if (soy < toy && sx0 <= tox) {
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }];
}
if (y < smy1 && sox > tx0) {
const x = Math.max(smx1, tmx1);
const y = Math.min(smy0, tmy0);
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }
];
} else if (sourceSide === 'right' && targetSide === 'bottom') {
const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint);
// The target point is inside the source element
if (isPointInsideSource) {
if (soy <= tmy1) {
const x = Math.max(smx1, tmx1);
const y = Math.max(soy + sourceMargin, toy);
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
// Target anchor is above the source anchor
const anchorMiddleY = (soy - sourceMargin + toy) / 2;
return [
{ x: sox, y: soy },
{ x: sox, y: anchorMiddleY },
{ x: tox, y: anchorMiddleY }
];
}
if (smx1 < tox) {
if (soy < toy) {
let x = tmx1;
if (tmx0 >= smx1 && tmy1 >= soy) {
x = middleOfVerticalSides;
}
return [
{ x, y: soy },
{ x, y: toy },
{ x: tox, y: toy }
];
}
return [{ x: tox, y: soy }];
}
const y = Math.max(tmy1, middleOfHorizontalSides);
if (soy > toy && sx0 <= tox) {
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }
];
}
if (y > smy0 && sox > tx0) {
const x = Math.max(smx1, tmx1);
const y = Math.max(smy1, tmy1);
return [
{ x, y: soy },
{ x, y },
{ x: tox, y }
];
}
return [
{ x: sox, y: soy },
{ x: sox, y },
{ x: tox, y }
];
}
}
function getLoopCoordinates(direction, angle, margin) {
const isHorizontal = direction === Directions.LEFT || direction === Directions.RIGHT;
let dx = 0;
let dy = 0;
switch (g.normalizeAngle(Math.round(angle))) {
case 0:
case 90:
dx = isHorizontal ? 0 : margin;
dy = isHorizontal ? margin : 0;
break;
case 180:
case 270:
dx = isHorizontal ? 0 : -margin;
dy = isHorizontal ? -margin : 0;
break;
}
return { dx, dy };
}
function rightAngleRouter(vertices, opt, linkView) {
const { sourceDirection = Directions.AUTO, targetDirection = Directions.AUTO } = opt;
const margin = opt.margin || 20;
const useVertices = opt.useVertices || false;
const isSourcePort = !!linkView.model.source().port;
const sourcePoint = pointDataFromAnchor(linkView.sourceView, linkView.sourceAnchor, linkView.sourceBBox, sourceDirecti