@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
279 lines • 12.1 kB
JavaScript
/* eslint-disable no-underscore-dangle */
import { Point, Line, Path } from '../../geometry';
// takes care of math. error for case when jump is too close to end of line
const CLOSE_PROXIMITY_PADDING = 1;
const F13 = 1 / 3;
const F23 = 2 / 3;
function setupUpdating(view) {
let updateList = view.graph._jumpOverUpdateList;
// first time setup for this paper
if (updateList == null) {
updateList = view.graph._jumpOverUpdateList = [];
/**
* Handler for a batch:stop event to force
* update of all registered links with jump over connector
*/
view.graph.on('cell:mouseup', () => {
const list = view.graph._jumpOverUpdateList;
for (let i = 0; i < list.length; i += 1) {
list[i].update();
}
});
view.graph.on('model:reseted', () => {
updateList = view.graph._jumpOverUpdateList = [];
});
}
// add this link to a list so it can be updated when some other link is updated
if (updateList.indexOf(view) < 0) {
updateList.push(view);
// watch for change of connector type or removal of link itself
// to remove the link from a list of jump over connectors
const clean = () => updateList.splice(updateList.indexOf(view), 1);
view.cell.once('change:connector', clean);
view.cell.once('removed', clean);
}
}
function createLines(sourcePoint, targetPoint, route = []) {
const points = [sourcePoint, ...route, targetPoint];
const lines = [];
points.forEach((point, idx) => {
const next = points[idx + 1];
if (next != null) {
lines.push(new Line(point, next));
}
});
return lines;
}
function findLineIntersections(line, crossCheckLines) {
const intersections = [];
crossCheckLines.forEach((crossCheckLine) => {
const intersection = line.intersectsWithLine(crossCheckLine);
if (intersection) {
intersections.push(intersection);
}
});
return intersections;
}
function getDistence(p1, p2) {
return new Line(p1, p2).squaredLength();
}
/**
* Split input line into multiple based on intersection points.
*/
function createJumps(line, intersections, jumpSize) {
return intersections.reduce((memo, point, idx) => {
// skipping points that were merged with the previous line
// to make bigger arc over multiple lines that are close to each other
if (skippedPoints.includes(point)) {
return memo;
}
// always grab the last line from buffer and modify it
const lastLine = memo.pop() || line;
// calculate start and end of jump by moving by a given size of jump
const jumpStart = Point.create(point).move(lastLine.start, -jumpSize);
let jumpEnd = Point.create(point).move(lastLine.start, +jumpSize);
// now try to look at the next intersection point
const nextPoint = intersections[idx + 1];
if (nextPoint != null) {
const 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);
skippedPoints.push(nextPoint);
}
}
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
const 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) {
memo.push(lastLine);
return memo;
}
}
const 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
memo.push(lastLine);
return memo;
}
// finally create a jump line
const jumpLine = new Line(jumpStart, jumpEnd);
// it's just simple line but with a `isJump` property
jumppedLines.push(jumpLine);
memo.push(new Line(lastLine.start, jumpStart), jumpLine, new Line(jumpEnd, lastLine.end));
return memo;
}, []);
}
function buildPath(lines, jumpSize, jumpType, radius) {
const path = new Path();
let segment;
// first move to the start of a first line
segment = Path.createSegment('M', lines[0].start);
path.appendSegment(segment);
lines.forEach((line, index) => {
if (jumppedLines.includes(line)) {
let angle;
let diff;
let control1;
let control2;
if (jumpType === 'arc') {
// approximates semicircle with 2 curves
angle = -90;
// determine rotation of arc based on difference between points
diff = line.start.diff(line.end);
// make sure the arc always points up (or right)
const xAxisRotate = diff.x < 0 || (diff.x === 0 && diff.y < 0);
if (xAxisRotate) {
angle += 180;
}
const center = line.getCenter();
const centerLine = new Line(center, line.end).rotate(angle, center);
let halfLine;
// first half
halfLine = new Line(line.start, center);
control1 = halfLine.pointAt(2 / 3).rotate(angle, line.start);
control2 = centerLine.pointAt(1 / 3).rotate(-angle, centerLine.end);
segment = Path.createSegment('C', control1, control2, centerLine.end);
path.appendSegment(segment);
// second half
halfLine = new Line(center, line.end);
control1 = centerLine.pointAt(1 / 3).rotate(angle, centerLine.end);
control2 = halfLine.pointAt(1 / 3).rotate(-angle, line.end);
segment = Path.createSegment('C', control1, control2, line.end);
path.appendSegment(segment);
}
else if (jumpType === 'gap') {
segment = Path.createSegment('M', line.end);
path.appendSegment(segment);
}
else if (jumpType === 'cubic') {
// approximates semicircle with 1 curve
angle = line.start.theta(line.end);
const xOffset = jumpSize * 0.6;
let yOffset = jumpSize * 1.35;
// determine rotation of arc based on difference between points
diff = line.start.diff(line.end);
// make sure the arc always points up (or right)
const xAxisRotate = diff.x < 0 || (diff.x === 0 && diff.y < 0);
if (xAxisRotate) {
yOffset *= -1;
}
control1 = new Point(line.start.x + xOffset, line.start.y + yOffset).rotate(angle, line.start);
control2 = new Point(line.end.x - xOffset, line.end.y + yOffset).rotate(angle, line.end);
segment = Path.createSegment('C', control1, control2, line.end);
path.appendSegment(segment);
}
}
else {
const nextLine = lines[index + 1];
if (radius === 0 || !nextLine || jumppedLines.includes(nextLine)) {
segment = Path.createSegment('L', line.end);
path.appendSegment(segment);
}
else {
buildRoundedSegment(radius, path, line.end, line.start, nextLine.end);
}
}
});
return path;
}
function buildRoundedSegment(offset, path, curr, prev, next) {
const prevDistance = curr.distance(prev) / 2;
const nextDistance = curr.distance(next) / 2;
const startMove = -Math.min(offset, prevDistance);
const endMove = -Math.min(offset, nextDistance);
const roundedStart = curr.clone().move(prev, startMove).round();
const roundedEnd = curr.clone().move(next, endMove).round();
const control1 = new Point(F13 * roundedStart.x + F23 * curr.x, F23 * curr.y + F13 * roundedStart.y);
const control2 = new Point(F13 * roundedEnd.x + F23 * curr.x, F23 * curr.y + F13 * roundedEnd.y);
let segment;
segment = Path.createSegment('L', roundedStart);
path.appendSegment(segment);
segment = Path.createSegment('C', control1, control2, roundedEnd);
path.appendSegment(segment);
}
let jumppedLines;
let skippedPoints;
export const jumpover = function (sourcePoint, targetPoint, routePoints, options = {}) {
jumppedLines = [];
skippedPoints = [];
setupUpdating(this);
const jumpSize = options.size || 5;
const jumpType = options.type || 'arc';
const radius = options.radius || 0;
// list of connector types not to jump over.
const ignoreConnectors = options.ignoreConnectors || ['smooth'];
const graph = this.graph;
const model = graph.model;
const allLinks = model.getEdges();
// there is just one link, draw it directly
if (allLinks.length === 1) {
return buildPath(createLines(sourcePoint, targetPoint, routePoints), jumpSize, jumpType, radius);
}
const edge = this.cell;
const thisIndex = allLinks.indexOf(edge);
const defaultConnector = graph.options.connecting.connector || {};
// not all links are meant to be jumped over.
const edges = allLinks.filter((link, idx) => {
const connector = link.getConnector() || defaultConnector;
// avoid jumping over links with connector type listed in `ignored connectors`.
if (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';
}
return true;
});
// find views for all links
const linkViews = edges.map((edge) => {
return graph.renderer.findViewByCell(edge);
});
// create lines for this link
const thisLines = createLines(sourcePoint, targetPoint, routePoints);
// create lines for all other links
const linkLines = linkViews.map((linkView) => {
if (linkView == null) {
return [];
}
if (linkView === this) {
return thisLines;
}
return createLines(linkView.sourcePoint, linkView.targetPoint, linkView.routePoints);
});
// transform lines for this link by splitting with jump lines at
// points of intersection with other links
const jumpingLines = [];
thisLines.forEach((line) => {
// iterate all links and grab the intersections with this line
// these are then sorted by distance so the line can be split more easily
const intersections = edges
.reduce((memo, link, i) => {
// don't intersection with itself
if (link !== edge) {
const lineIntersections = findLineIntersections(line, linkLines[i]);
memo.push(...lineIntersections);
}
return memo;
}, [])
.sort((a, b) => getDistence(line.start, a) - getDistence(line.start, b));
if (intersections.length > 0) {
// split the line based on found intersection points
jumpingLines.push(...createJumps(line, intersections, jumpSize));
}
else {
// without any intersection the line goes uninterrupted
jumpingLines.push(line);
}
});
const path = buildPath(jumpingLines, jumpSize, jumpType, radius);
jumppedLines = [];
skippedPoints = [];
return options.raw ? path : path.serialize();
};
//# sourceMappingURL=jumpover.js.map