UNPKG

jointjs

Version:

JavaScript diagramming library

453 lines (372 loc) 16 kB
import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; // default size of jump if not specified in options var JUMP_SIZE = 5; // available jump types // first one taken as default var JUMP_TYPES = ['arc', 'gap', 'cubic']; // default radius var RADIUS = 0; // takes care of math. error for case when jump is too close to end of line var CLOSE_PROXIMITY_PADDING = 1; // list of connector types not to jump over. var IGNORED_CONNECTORS = ['smooth']; // internal constants for round segment var _13 = 1 / 3; var _23 = 2 / 3; function sortPointsAscending(p1, p2) { let { x: x1, y: y1 } = p1; let { x: x2, y: y2 } = p2; if (x1 > x2) { let swap = x1; x1 = x2; x2 = swap; swap = y1; y1 = y2; y2 = swap; } if (y1 > y2) { let swap = x1; x1 = x2; x2 = swap; swap = y1; y1 = y2; y2 = swap; } return [new g.Point(x1, y1), new g.Point(x2, y2)]; } function overlapExists(line1, line2) { const [{ x: x1, y: y1 }, { x: x2, y: y2 }] = sortPointsAscending(line1.start, line1.end); const [{ x: x3, y: y3 }, { x: x4, y: y4 }] = sortPointsAscending(line2.start, line2.end); const xMatch = x1 <= x4 && x3 <= x2; const yMatch = y1 <= y4 && y3 <= y2; return xMatch && yMatch; } /** * Transform start/end and route into series of lines * @param {g.point} sourcePoint start point * @param {g.point} targetPoint end point * @param {g.point[]} route optional list of route * @return {g.line[]} [description] */ function createLines(sourcePoint, targetPoint, route) { // make a flattened array of all points var points = [].concat(sourcePoint, route, targetPoint); return points.reduce(function(resultLines, point, idx) { // if there is a next point, make a line with it var nextPoint = points[idx + 1]; if (nextPoint != null) { resultLines[idx] = g.line(point, nextPoint); } return resultLines; }, []); } function setupUpdating(jumpOverLinkView) { var paper = jumpOverLinkView.paper; var updateList = paper._jumpOverUpdateList; // first time setup for this paper if (updateList == null) { updateList = paper._jumpOverUpdateList = []; var graph = paper.model; graph.on('batch:stop', function() { if (this.hasActiveBatch()) return; updateJumpOver(paper); }); graph.on('reset', function() { updateList = paper._jumpOverUpdateList = []; }); } // add this link to a list so it can be updated when some other link is updated if (updateList.indexOf(jumpOverLinkView) < 0) { updateList.push(jumpOverLinkView); // watch for change of connector type or removal of link itself // to remove the link from a list of jump over connectors jumpOverLinkView.listenToOnce(jumpOverLinkView.model, 'change:connector remove', function() { updateList.splice(updateList.indexOf(jumpOverLinkView), 1); }); } } /** * Handler for a batch:stop event to force * update of all registered links with jump over connector * @param {object} batchEvent optional object with info about batch */ function updateJumpOver(paper) { var updateList = paper._jumpOverUpdateList; for (var i = 0; i < updateList.length; i++) { const linkView = updateList[i]; const updateFlag = linkView.getFlag(linkView.constructor.Flags.CONNECTOR); linkView.requestUpdate(updateFlag); } } /** * Utility function to collect all intersection points of a single * line against group of other lines. * @param {g.line} line where to find points * @param {g.line[]} crossCheckLines lines to cross * @return {g.point[]} list of intersection points */ function findLineIntersections(line, crossCheckLines) { return util.toArray(crossCheckLines).reduce(function(res, crossCheckLine) { var intersection = line.intersection(crossCheckLine); if (intersection) { res.push(intersection); } return res; }, []); } /** * Sorting function for list of points by their distance. * @param {g.point} p1 first point * @param {g.point} p2 second point * @return {number} squared distance between points */ function sortPoints(p1, p2) { return g.line(p1, p2).squaredLength(); } /** * Split input line into multiple based on intersection points. * @param {g.line} line input line to split * @param {g.point[]} intersections points where to split the line * @param {number} jumpSize the size of jump arc (length empty spot on a line) * @return {g.line[]} list of lines being split */ function createJumps(line, intersections, jumpSize) { return intersections.reduce(function(resultLines, 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 (point.skip === true) { return resultLines; } // always grab the last line from buffer and modify it var lastLine = resultLines.pop() || line; // 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)); // 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 resultLines.push(lastLine); return resultLines; } // 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), jumpLine, g.line(jumpEnd, lastLine.end) ); return resultLines; }, []); } /** * Assemble `D` attribute of a SVG path by iterating given lines. * @param {g.line[]} lines source lines to use * @param {number} jumpSize the size of jump arc (length empty spot on a line) * @param {number} radius the radius * @return {string} */ function buildPath(lines, jumpSize, jumpType, radius) { var path = new g.Path(); var segment; // first move to the start of a first line segment = g.Path.createSegment('M', lines[0].start); path.appendSegment(segment); // make a paths from lines util.toArray(lines).forEach(function(line, index) { if (line.isJump) { var angle, diff; var control1, control2; if (jumpType === 'arc') { // approximates semicircle with 2 curves angle = -90; // 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)); if (xAxisRotate) angle += 180; var midpoint = line.midpoint(); var centerLine = new g.Line(midpoint, line.end).rotate(midpoint, angle); var halfLine; // first half halfLine = new g.Line(line.start, midpoint); control1 = halfLine.pointAt(2 / 3).rotate(line.start, angle); control2 = centerLine.pointAt(1 / 3).rotate(centerLine.end, -angle); segment = g.Path.createSegment('C', control1, control2, centerLine.end); path.appendSegment(segment); // second half halfLine = new g.Line(midpoint, line.end); control1 = centerLine.pointAt(1 / 3).rotate(centerLine.end, angle); control2 = halfLine.pointAt(1 / 3).rotate(line.end, -angle); segment = g.Path.createSegment('C', control1, control2, line.end); path.appendSegment(segment); } else if (jumpType === 'gap') { segment = g.Path.createSegment('M', line.end); path.appendSegment(segment); } else if (jumpType === 'cubic') { // approximates semicircle with 1 curve angle = line.start.theta(line.end); var xOffset = jumpSize * 0.6; var yOffset = jumpSize * 1.35; // 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)); if (xAxisRotate) yOffset *= -1; control1 = g.Point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); control2 = g.Point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); segment = g.Path.createSegment('C', control1, control2, line.end); path.appendSegment(segment); } } else { var nextLine = lines[index + 1]; if (radius == 0 || !nextLine || nextLine.isJump) { segment = g.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) { var prevDistance = curr.distance(prev) / 2; var nextDistance = curr.distance(next) / 2; var startMove = -Math.min(offset, prevDistance); var endMove = -Math.min(offset, nextDistance); var roundedStart = curr.clone().move(prev, startMove).round(); var roundedEnd = curr.clone().move(next, endMove).round(); var control1 = new g.Point((_13 * roundedStart.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedStart.y)); var control2 = new g.Point((_13 * roundedEnd.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedEnd.y)); var segment; segment = g.Path.createSegment('L', roundedStart); path.appendSegment(segment); segment = g.Path.createSegment('C', control1, control2, roundedEnd); path.appendSegment(segment); } /** * 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 * @return {string} created `D` attribute of SVG path */ export const jumpover = function(sourcePoint, targetPoint, route, opt) { // eslint-disable-line max-params setupUpdating(this); var raw = opt.raw; var jumpSize = opt.size || JUMP_SIZE; var jumpType = opt.jump && ('' + opt.jump).toLowerCase(); var radius = opt.radius || RADIUS; var ignoreConnectors = opt.ignoreConnectors || IGNORED_CONNECTORS; // grab the first jump type as a default if specified one is invalid if (JUMP_TYPES.indexOf(jumpType) === -1) { jumpType = JUMP_TYPES[0]; } var paper = this.paper; var graph = paper.model; var allLinks = graph.getLinks(); // there is just one link, draw it directly if (allLinks.length === 1) { return buildPath( createLines(sourcePoint, targetPoint, route), jumpSize, jumpType, radius ); } var thisModel = this.model; var thisIndex = allLinks.indexOf(thisModel); var defaultConnector = paper.options.defaultConnector || {}; // not all links are meant to be jumped over. var links = allLinks.filter(function(link, idx) { var connector = link.get('connector') || defaultConnector; // avoid jumping over links with connector type listed in `ignored connectors`. 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'; } return true; }); // find views for all links var linkViews = links.map(function(link) { return paper.findViewByModel(link); }); // create lines for this link var thisLines = createLines( sourcePoint, targetPoint, route ); // create lines for all other links var linkLines = linkViews.map(function(linkView) { if (linkView == null) { return []; } if (linkView === this) { return thisLines; } return createLines( linkView.sourcePoint, linkView.targetPoint, linkView.route ); }, this); // transform lines for this link by splitting with jump lines at // points of intersection with other links var jumpingLines = thisLines.reduce(function(resultLines, thisLine) { // iterate all links and grab the intersections with this line // these are then sorted by distance so the line can be split more easily var intersections = links.reduce(function(res, link, i) { // don't intersection with itself if (link !== thisModel) { const linkLinesToTest = linkLines[i].slice(); const overlapIndex = linkLinesToTest.findIndex((line) => overlapExists(thisLine, line)); // Overlap occurs and the end point of one segment lies on thisLine if (overlapIndex > -1 && thisLine.containsPoint(linkLinesToTest[overlapIndex].end)) { // Remove the next segment because there will never be a jump linkLinesToTest.splice(overlapIndex + 1, 1); } const lineIntersections = findLineIntersections(thisLine, linkLinesToTest); res.push.apply(res, lineIntersections); } return res; }, []).sort(function(a, b) { return sortPoints(thisLine.start, a) - sortPoints(thisLine.start, b); }); if (intersections.length > 0) { // split the line based on found intersection points resultLines.push.apply(resultLines, createJumps(thisLine, intersections, jumpSize)); } else { // without any intersection the line goes uninterrupted resultLines.push(thisLine); } return resultLines; }, []); var path = buildPath(jumpingLines, jumpSize, jumpType, radius); return (raw) ? path : path.serialize(); };