UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

405 lines (339 loc) 12.5 kB
/* eslint-disable no-underscore-dangle */ import { Line, Path, Point, type PointLike } from '../../geometry' import type { Edge } from '../../model' import type { EdgeView } from '../../view' import type { ConnectorBaseOptions, ConnectorDefinition } from './index' // 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 let jumppedLines: Line[] = [] let skippedPoints: Point[] = [] export function setupUpdating(view: EdgeView) { let updateList = (view.graph as any)._jumpOverUpdateList // first time setup for this paper if (updateList == null) { updateList = (view.graph as any)._jumpOverUpdateList = [] view.graph.on('cell:mouseup', () => { const list = (view.graph as any)._jumpOverUpdateList // add timeout to wait for the target node to be connected // fix https://github.com/antvis/X6/issues/3387 setTimeout(() => { for (let i = 0; i < list.length; i += 1) { list[i].update() } }) }) view.graph.on('model:reseted', () => { updateList = (view.graph as any)._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) } } export function createLines( sourcePoint: PointLike, targetPoint: PointLike, route: PointLike[] = [], ) { const points = [sourcePoint, ...route, targetPoint] const lines: Line[] = [] points.forEach((point, idx) => { const next = points[idx + 1] if (next != null) { lines.push(new Line(point, next)) } }) return lines } export function findLineIntersections(line: Line, crossCheckLines: Line[]) { const intersections: Point[] = [] crossCheckLines.forEach((crossCheckLine) => { const intersection = line.intersectsWithLine(crossCheckLine) if (intersection) { const { x, y } = intersection const { start, end } = crossCheckLine const startIsIntersection = Math.round(start.x) === Math.round(x) && Math.round(start.y) === Math.round(y) const endIsIntersection = Math.round(end.x) === Math.round(x) && Math.round(end.y) === Math.round(y) // If the starting or ending point is the same as the intersection point, return if (startIsIntersection || endIsIntersection) { return } intersections.push(intersection) } }) return intersections } export function getDistence(p1: Point, p2: Point) { return new Line(p1, p2).squaredLength() } /** * Split input line into multiple based on intersection points. */ export function createJumps( line: Line, intersections: Point[], jumpSize: number, ) { return intersections.reduce<Line[]>((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 }, []) } export function buildPath( lines: Line[], jumpSize: number, jumpType: JumpType, radius: number, ) { 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 } export function buildRoundedSegment( offset: number, path: Path, curr: Point, prev: Point, next: Point, ) { 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) } export type JumpType = 'arc' | 'gap' | 'cubic' export interface JumpoverConnectorOptions extends ConnectorBaseOptions { size?: number radius?: number type?: JumpType ignoreConnectors?: string[] } export const jumpover: ConnectorDefinition<JumpoverConnectorOptions> = 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() as Edge[] // 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 as any) // 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.findViewByCell(edge) as EdgeView }) // 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: Line[] = [] 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<Point[]>((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() }