@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
389 lines (323 loc) • 11.9 kB
text/typescript
/* eslint-disable no-underscore-dangle */
import { Point, Line, Path } from '../../geometry'
import { Edge } from '../../model'
import { EdgeView } from '../../view'
import { Connector } 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
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 = []
/**
* 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 as any)._jumpOverUpdateList
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)
}
}
function createLines(
sourcePoint: Point.PointLike,
targetPoint: Point.PointLike,
route: Point.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
}
function findLineIntersections(line: Line, crossCheckLines: Line[]) {
const intersections: Point[] = []
crossCheckLines.forEach((crossCheckLine) => {
const intersection = line.intersectsWithLine(crossCheckLine)
if (intersection) {
intersections.push(intersection)
}
})
return intersections
}
function getDistence(p1: Point, p2: Point) {
return new Line(p1, p2).squaredLength()
}
/**
* Split input line into multiple based on intersection points.
*/
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
}, [])
}
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
}
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 Connector.BaseOptions {
size?: number
radius?: number
type?: JumpType
ignoreConnectors?: string[]
}
let jumppedLines: Line[]
let skippedPoints: Point[]
export const jumpover: Connector.Definition<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.renderer.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()
}