Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/jumpover-jump-direction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@joint/core": patch
---

connectors.jumpover - fix jumps on a nearly vertical line (e.g. computed by a router) pointing to the opposite side
5 changes: 5 additions & 0 deletions .changeset/jumpover-jump-priority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@joint/core": minor
---

connectors.jumpover - add `jumpPriority` option to choose which of two crossing links draws the jump
5 changes: 5 additions & 0 deletions .changeset/jumpover-skip-distant-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@joint/core": patch
---

connectors.jumpover - skip links whose paths cannot cross when looking for jumps
5 changes: 5 additions & 0 deletions .changeset/jumpover-track-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@joint/core": patch
---

connectors.jumpover - draw one jump over a track of close links, no higher than a single jump
160 changes: 125 additions & 35 deletions packages/joint-core/src/connectors/jumpover.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as util from '../util/index.mjs';
import * as g from '../g/index.mjs';
import V from '../V/index.mjs';

// default size of jump if not specified in options
var JUMP_SIZE = 5;
Expand All @@ -21,6 +22,13 @@ var IGNORED_CONNECTORS = ['smooth'];
var _13 = 1 / 3;
var _23 = 2 / 3;

// distance under which two coordinates are considered equal
const TOLERANCE = 1e-6;

// which of two crossing jumpover links draws the jump
// first one taken as default
const JUMP_PRIORITIES = ['paint-order', 'vertical', 'horizontal'];

/**
* Transform start/end and route into series of lines
* @param {g.point} sourcePoint start point
Expand All @@ -41,6 +49,10 @@ function createLines(sourcePoint, targetPoint, route) {
}, []);
}

function getPathBBox(sourcePoint, targetPoint, route) {
return new g.Polyline([].concat(sourcePoint, route || [], targetPoint)).bbox();
}

function setupUpdating(jumpOverLinkView) {
var paper = jumpOverLinkView.paper;
var updateList = paper._jumpOverUpdateList;
Expand Down Expand Up @@ -128,53 +140,55 @@ function createJumps(line, intersections, jumpSize) {

// always grab the last line from buffer and modify it
var lastLine = resultLines.pop() || line;
const start = lastLine.start;

// calculate start and end of jump by moving by a given size of jump
var jumpStart = g.point(point).move(lastLine.start, -(jumpSize));
var jumpEnd = g.point(point).move(lastLine.start, +(jumpSize));
// the space a jump needs before the first and after the last crossing
const margin = jumpSize + CLOSE_PROXIMITY_PADDING;

// now try to look at the next intersection point
var nextPoint = intersections[idx + 1];
if (nextPoint != null) {
var distance = jumpEnd.distance(nextPoint);
if (distance <= jumpSize) {
// next point is close enough, move the jump end by this
// difference and mark the next point to be skipped
jumpEnd = nextPoint.move(lastLine.start, distance);
nextPoint.skip = true;
}
} else {
// this block is inside of `else` as an optimization so the distance is
// not calculated when we know there are no other intersection points
var endDistance = jumpStart.distance(lastLine.end);
// if the end is too close to possible jump, draw remaining line instead of a jump
if (endDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) {
resultLines.push(lastLine);
return resultLines;
}
}

var startDistance = jumpEnd.distance(lastLine.start);
if (startDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) {
// if the start of line is too close to jump, draw that line instead of a jump
// if the start or the end of the line is too close to the jump,
// draw that line instead of a jump
if (start.distance(point) < margin || lastLine.end.distance(point) < margin) {
resultLines.push(lastLine);
return resultLines;
}

// calculate start and end of jump by moving by a given size of jump
const jumpStart = g.point(point).move(start, -(jumpSize));
let jumpEnd = g.point(point).move(start, +(jumpSize));

// Merge every following intersection that is close to the end of the
// jump, so a track of parallel lines gets one jump instead of many.
for (let next = idx + 1; next < intersections.length; next++) {
const nextPoint = intersections[next];
if (start.distance(nextPoint) - start.distance(jumpEnd) > jumpSize) break;
// leave the intersections too close to the end out of the jump
if (lastLine.end.distance(nextPoint) < margin) break;
nextPoint.skip = true;
jumpEnd = g.point(nextPoint).move(start, +(jumpSize));
}

// finally create a jump line
var jumpLine = g.line(jumpStart, jumpEnd);
// it's just simple line but with a `isJump` property
jumpLine.isJump = true;

resultLines.push(
g.line(lastLine.start, jumpStart),
g.line(start, jumpStart),
jumpLine,
g.line(jumpEnd, lastLine.end)
);
return resultLines;
}, []);
}

// Whether the jump has to be flipped so it points up (or right). A vertical
// line computed by a router can be a tiny bit off (e.g. `x` differs by 1e-13),
// which must not flip the jump.
function pointsLeftOrDown(diff) {
if (Math.abs(diff.x) > TOLERANCE) return Number(diff.x < 0);
return Number(diff.y < 0);
}

/**
* Assemble `D` attribute of a SVG path by iterating given lines.
* @param {g.line[]} lines source lines to use
Expand Down Expand Up @@ -204,12 +218,34 @@ function buildPath(lines, jumpSize, jumpType, radius) {
// determine rotation of arc based on difference between points
diff = line.start.difference(line.end);
// make sure the arc always points up (or right)
var xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0));
var xAxisRotate = pointsLeftOrDown(diff);
if (xAxisRotate) angle += 180;

var midpoint = line.midpoint();
var centerLine = new g.Line(midpoint, line.end).rotate(midpoint, angle);

if (line.length() > 2 * jumpSize + TOLERANCE) {
// A merged jump over several lines: a quarter circle up,
// a straight top and a quarter circle down, so it bulges
// no more than a single jump (by `jumpSize`).
const along = new g.Point(line.end).difference(line.start).normalize(jumpSize);
const normal = new g.Point(centerLine.end).difference(midpoint).normalize(jumpSize);
const topStart = new g.Point(line.start).offset(along.x + normal.x, along.y + normal.y);
const topEnd = new g.Point(line.end).offset(-along.x + normal.x, -along.y + normal.y);
path.appendSegment(g.Path.createSegment('C',
new g.Point(line.start).offset(normal.x * V.KAPPA, normal.y * V.KAPPA),
new g.Point(topStart).offset(-along.x * V.KAPPA, -along.y * V.KAPPA),
topStart
));
path.appendSegment(g.Path.createSegment('L', topEnd));
path.appendSegment(g.Path.createSegment('C',
new g.Point(topEnd).offset(along.x * V.KAPPA, along.y * V.KAPPA),
new g.Point(line.end).offset(normal.x * V.KAPPA, normal.y * V.KAPPA),
line.end
));
return;
}

var halfLine;

// first half
Expand Down Expand Up @@ -243,7 +279,7 @@ function buildPath(lines, jumpSize, jumpType, radius) {
// determine rotation of arc based on difference between points
diff = line.start.difference(line.end);
// make sure the arc always points up (or right)
xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0));
xAxisRotate = pointsLeftOrDown(diff);
if (xAxisRotate) yOffset *= -1;

control1 = g.Point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle);
Expand Down Expand Up @@ -288,13 +324,38 @@ function buildRoundedSegment(offset, path, curr, prev, next) {
path.appendSegment(segment);
}

// How vertical a line is: 0 for a horizontal line, 1 for a vertical one.
function getVerticality(line) {
const dx = Math.abs(line.end.x - line.start.x);
const dy = Math.abs(line.end.y - line.start.y);
return (dx + dy === 0) ? 0 : dy / (dx + dy);
}

function getJumpPriority(args) {
const jumpPriority = args && args.jumpPriority;
return (JUMP_PRIORITIES.indexOf(jumpPriority) === -1) ? JUMP_PRIORITIES[0] : jumpPriority;
}

// Whether `thisLine` draws the jump where it crosses `otherLine` of another
// jumpover link. The `jumpPriority` of the link painted on top applies, and
// both links evaluate it from their own side, so exactly one of them draws
// the jump.
function drawsJump(thisLine, otherLine, jumpPriority, isOtherAbove) {
const difference = getVerticality(thisLine) - getVerticality(otherLine);
if (Math.abs(difference) > TOLERANCE) {
return (jumpPriority === 'vertical') ? difference > 0 : difference < 0;
}
return !isOtherAbove;
}

/**
* Actual connector function that will be run on every update.
* @param {g.point} sourcePoint start point of this link
* @param {g.point} targetPoint end point of this link
* @param {g.point[]} route of this link
* @param {object} opt options
* @property {number} size optional size of a jump arc
* @property {string} jumpPriority which of two crossing jumpover links draws the jump
* @return {string} created `D` attribute of SVG path
*/
export const jumpover = function(sourcePoint, targetPoint, route, opt, linkView) {
Expand All @@ -306,6 +367,7 @@ export const jumpover = function(sourcePoint, targetPoint, route, opt, linkView)
var jumpType = opt.jump && ('' + opt.jump).toLowerCase();
var radius = opt.radius || RADIUS;
var ignoreConnectors = opt.ignoreConnectors || IGNORED_CONNECTORS;
const jumpPriority = getJumpPriority(opt);

// grab the first jump type as a default if specified one is invalid
if (JUMP_TYPES.indexOf(jumpType) === -1) {
Expand All @@ -328,6 +390,10 @@ export const jumpover = function(sourcePoint, targetPoint, route, opt, linkView)
var thisIndex = allLinks.indexOf(thisModel);
var defaultConnector = paper.options.defaultConnector || {};

// Jumpover links that do not leave all their crossings with this link to
// the link painted on top: which of the two draws each jump.
const jumpRules = new Map();

// not all links are meant to be jumped over.
var links = allLinks.filter(function(link, idx) {

Expand All @@ -337,10 +403,16 @@ export const jumpover = function(sourcePoint, targetPoint, route, opt, linkView)
if (util.toArray(ignoreConnectors).includes(connector.name)) {
return false;
}
// filter out links that are above this one and have the same connector type
// otherwise there would double hoops for each intersection
if (idx > thisIndex) {
return connector.name !== 'jumpover';
if (link !== thisModel && connector.name === 'jumpover') {
const isAbove = idx > thisIndex;
const priority = isAbove ? getJumpPriority(connector.args) : jumpPriority;
if (priority !== 'paint-order') {
jumpRules.set(link, { priority, isAbove });
} else if (isAbove) {
// filter out links that are above this one and have the same connector type
// otherwise there would double hoops for each intersection
return false;
}
}
return true;
});
Expand All @@ -350,6 +422,19 @@ export const jumpover = function(sourcePoint, targetPoint, route, opt, linkView)
return paper.findViewByModel(link);
});

// Two links can cross only where the bounding boxes of their routes meet,
// so the links outside of this link's bounding box are left out.
// It is inflated, since `g.Rect.intersect()` misses boxes that only touch
// (e.g. the zero-height box of a horizontal link).
const thisBBox = getPathBBox(sourcePoint, targetPoint, route).inflate(1);
const isCandidate = links.map((link, i) => {
const view = linkViews[i];
if (link === thisModel || !view || !view.sourcePoint || !view.targetPoint) return true;
return thisBBox.intersect(getPathBBox(view.sourcePoint, view.targetPoint, view.route)) !== null;
});
links = links.filter((_, i) => isCandidate[i]);
linkViews = linkViews.filter((_, i) => isCandidate[i]);

// create lines for this link
var thisLines = createLines(
sourcePoint,
Expand Down Expand Up @@ -388,7 +473,12 @@ export const jumpover = function(sourcePoint, targetPoint, route, opt, linkView)
// leave such segments out.
const lines = linkLines[i];
const collinear = lines.map((line) => thisLine.isCollinear(line));
const linkLinesToTest = lines.filter((_, index) => !collinear[index - 1] && !collinear[index + 1]);
let linkLinesToTest = lines.filter((_, index) => !collinear[index - 1] && !collinear[index + 1]);
const jumpRule = jumpRules.get(link);
if (jumpRule) {
const { priority, isAbove } = jumpRule;
linkLinesToTest = linkLinesToTest.filter((line) => drawsJump(thisLine, line, priority, isAbove));
}
const lineIntersections = findLineIntersections(thisLine, linkLinesToTest);
res.push.apply(res, lineIntersections);
}
Expand Down
Loading
Loading