@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
330 lines (275 loc) • 8.65 kB
text/typescript
import { ArrayExt } from '../../common'
import {
toRad,
Line,
Point,
type Rectangle,
type PointLike,
} from '../../geometry'
import type { RouterDefinition } from './index'
import * as Util from './util'
export interface OrthRouterOptions extends Util.PaddingOptions {}
/**
* Returns a route with orthogonal line segments.
*/
export const orth: RouterDefinition<OrthRouterOptions> = (
vertices,
options,
edgeView,
) => {
let sourceBBox = Util.getSourceBBox(edgeView, options)
let targetBBox = Util.getTargetBBox(edgeView, options)
const sourceAnchor = Util.getSourceAnchor(edgeView, options)
const targetAnchor = Util.getTargetAnchor(edgeView, options)
// If anchor lies outside of bbox, the bbox expands to include it
sourceBBox = sourceBBox.union(Util.getPointBBox(sourceAnchor))
targetBBox = targetBBox.union(Util.getPointBBox(targetAnchor))
const points = vertices.map((p) => Point.create(p))
points.unshift(sourceAnchor)
points.push(targetAnchor)
// bearing of previous route segment
let bearing: Bearings | null = null
const result = []
for (let i = 0, len = points.length - 1; i < len; i += 1) {
let route = null
const from = points[i]
const to = points[i + 1]
const isOrthogonal = getBearing(from, to) != null
if (i === 0) {
// source
if (i + 1 === len) {
// source -> target
// Expand one of the nodes by 1px to detect situations when the two
// nodes are positioned next to each other with no gap in between.
if (sourceBBox.intersectsWithRect(targetBBox.clone().inflate(1))) {
route = insideNode(from, to, sourceBBox, targetBBox)
} else if (!isOrthogonal) {
route = nodeToNode(from, to, sourceBBox, targetBBox)
}
} else {
// source -> vertex
if (sourceBBox.containsPoint(to)) {
route = insideNode(
from,
to,
sourceBBox,
Util.getPointBBox(to).moveAndExpand(Util.getPaddingBox(options)),
)
} else if (!isOrthogonal) {
route = nodeToVertex(from, to, sourceBBox)
}
}
} else if (i + 1 === len) {
// vertex -> target
// prevent overlaps with previous line segment
const isOrthogonalLoop = isOrthogonal && getBearing(to, from) === bearing
if (targetBBox.containsPoint(from) || isOrthogonalLoop) {
route = insideNode(
from,
to,
Util.getPointBBox(from).moveAndExpand(Util.getPaddingBox(options)),
targetBBox,
bearing,
)
} else if (!isOrthogonal) {
route = vertexToNode(from, to, targetBBox, bearing)
}
} else if (!isOrthogonal) {
// vertex -> vertex
route = vertexToVertex(from, to, bearing)
}
// set bearing for next iteration
if (route) {
result.push(...route.points)
bearing = route.direction as Bearings
} else {
// orthogonal route and not looped
bearing = getBearing(from, to)
}
// push `to` point to identified orthogonal vertices array
if (i + 1 < len) {
result.push(to)
}
}
return result
}
/**
* Bearing to opposite bearing map
*/
const opposites = {
N: 'S',
S: 'N',
E: 'W',
W: 'E',
}
/**
* Bearing to radians map
*/
const radians = {
N: (-Math.PI / 2) * 3,
S: -Math.PI / 2,
E: 0,
W: Math.PI,
}
/**
* Returns a point `p` where lines p,p1 and p,p2 are perpendicular
* and p is not contained in the given box
*/
function freeJoin(p1: Point, p2: Point, bbox: Rectangle) {
let p = new Point(p1.x, p2.y)
if (bbox.containsPoint(p)) {
p = new Point(p2.x, p1.y)
}
// kept for reference
// if (bbox.containsPoint(p)) {
// return null
// }
return p
}
/**
* Returns either width or height of a bbox based on the given bearing.
*/
function getBBoxSize(bbox: Rectangle, bearing: Bearings) {
return bbox[bearing === 'W' || bearing === 'E' ? 'width' : 'height']
}
type Bearings = ReturnType<typeof getBearing>
function getBearing(from: PointLike, to: PointLike) {
if (from.x === to.x) {
return from.y > to.y ? 'N' : 'S'
}
if (from.y === to.y) {
return from.x > to.x ? 'W' : 'E'
}
return null
}
function vertexToVertex(from: Point, to: Point, bearing: Bearings) {
const p1 = new Point(from.x, to.y)
const p2 = new Point(to.x, from.y)
const d1 = getBearing(from, p1)
const d2 = getBearing(from, p2)
const opposite = bearing ? opposites[bearing] : null
const p =
d1 === bearing || (d1 !== opposite && (d2 === opposite || d2 !== bearing))
? p1
: p2
return { points: [p], direction: getBearing(p, to) }
}
function nodeToVertex(from: Point, to: Point, fromBBox: Rectangle) {
const p = freeJoin(from, to, fromBBox)
return { points: [p], direction: getBearing(p, to) }
}
function vertexToNode(
from: Point,
to: Point,
toBBox: Rectangle,
bearing: Bearings,
) {
const points = [new Point(from.x, to.y), new Point(to.x, from.y)]
const freePoints = points.filter((p) => !toBBox.containsPoint(p))
const freeBearingPoints = freePoints.filter(
(p) => getBearing(p, from) !== bearing,
)
let p
if (freeBearingPoints.length > 0) {
// Try to pick a point which bears the same direction as the previous segment.
p = freeBearingPoints.filter((p) => getBearing(from, p) === bearing).pop()
p = p || freeBearingPoints[0]
return {
points: [p],
direction: getBearing(p, to),
}
}
{
// Here we found only points which are either contained in the element or they would create
// a link segment going in opposite direction from the previous one.
// We take the point inside element and move it outside the element in the direction the
// route is going. Now we can join this point with the current end (using freeJoin).
p = ArrayExt.difference(points, freePoints)[0]
const p2 = Point.create(to).move(p, -getBBoxSize(toBBox, bearing) / 2)
const p1 = freeJoin(p2, from, toBBox)
return {
points: [p1, p2],
direction: getBearing(p2, to),
}
}
}
function nodeToNode(
from: Point,
to: Point,
fromBBox: Rectangle,
toBBox: Rectangle,
) {
let route = nodeToVertex(to, from, toBBox)
const p1 = route.points[0]
if (fromBBox.containsPoint(p1)) {
route = nodeToVertex(from, to, fromBBox)
const p2 = route.points[0]
if (toBBox.containsPoint(p2)) {
const fromBorder = Point.create(from).move(
p2,
-getBBoxSize(fromBBox, getBearing(from, p2)) / 2,
)
const toBorder = Point.create(to).move(
p1,
-getBBoxSize(toBBox, getBearing(to, p1)) / 2,
)
const mid = new Line(fromBorder, toBorder).getCenter()
const startRoute = nodeToVertex(from, mid, fromBBox)
const endRoute = vertexToVertex(mid, to, startRoute.direction as Bearings)
route.points = [startRoute.points[0], endRoute.points[0]]
route.direction = endRoute.direction
}
}
return route
}
// Finds route for situations where one node is inside the other.
// Typically the route is directed outside the outer node first and
// then back towards the inner node.
function insideNode(
from: Point,
to: Point,
fromBBox: Rectangle,
toBBox: Rectangle,
bearing?: Bearings,
) {
const boundary = fromBBox.union(toBBox).inflate(1)
// start from the point which is closer to the boundary
const center = boundary.getCenter()
const reversed = center.distance(to) > center.distance(from)
const start = reversed ? to : from
const end = reversed ? from : to
let p1: Point
let p2: Point
let p3: Point
if (bearing) {
// Points on circle with radius equals 'W + H` are always outside the rectangle
// with width W and height H if the center of that circle is the center of that rectangle.
p1 = Point.fromPolar(
boundary.width + boundary.height,
radians[bearing],
start,
)
p1 = boundary.getNearestPointToPoint(p1).move(p1, -1)
} else {
p1 = boundary.getNearestPointToPoint(start).move(start, 1)
}
p2 = freeJoin(p1, end, boundary)
let points: Point[]
if (p1.round().equals(p2.round())) {
p2 = Point.fromPolar(
boundary.width + boundary.height,
toRad(p1.theta(start)) + Math.PI / 2,
end,
)
p2 = boundary.getNearestPointToPoint(p2).move(end, 1).round()
p3 = freeJoin(p1, p2, boundary)
points = reversed ? [p2, p3, p1] : [p1, p3, p2]
} else {
points = reversed ? [p2, p1] : [p1, p2]
}
const direction = reversed ? getBearing(p1, to) : getBearing(p2, to)
return {
points,
direction,
}
}