@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