@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
1,831 lines (1,588 loc) • 80.8 kB
text/typescript
import { KeyValue } from '../types'
import { Rectangle, Polyline, Point, Angle, Path, Line } from '../geometry'
import {
StringExt,
ObjectExt,
NumberExt,
FunctionExt,
Dom,
Vector,
} from '../util'
import {
Attr,
Router,
Connector,
NodeAnchor,
EdgeAnchor,
ConnectionPoint,
} from '../registry'
import { Cell } from '../model/cell'
import { Edge } from '../model/edge'
import { Markup } from './markup'
import { CellView } from './cell'
import { NodeView } from './node'
import { ToolsView } from './tool'
export class EdgeView<
Entity extends Edge = Edge,
Options extends EdgeView.Options = EdgeView.Options,
> extends CellView<Entity, Options> {
protected readonly POINT_ROUNDING = 2
public path: Path
public routePoints: Point[]
public sourceAnchor: Point
public targetAnchor: Point
public sourcePoint: Point
public targetPoint: Point
public sourceView: CellView | null
public targetView: CellView | null
public sourceMagnet: Element | null
public targetMagnet: Element | null
protected toolCache: Element
protected tool2Cache: Element
protected readonly markerCache: {
sourcePoint?: Point
targetPoint?: Point
sourceBBox?: Rectangle
targetBBox?: Rectangle
} = {}
protected get [Symbol.toStringTag]() {
return EdgeView.toStringTag
}
protected getContainerClassName() {
return [super.getContainerClassName(), this.prefixClassName('edge')].join(
' ',
)
}
get sourceBBox() {
const sourceView = this.sourceView
if (!sourceView) {
const sourceDef = this.cell.getSource() as Edge.TerminalPointData
return new Rectangle(sourceDef.x, sourceDef.y)
}
const sourceMagnet = this.sourceMagnet
if (sourceView.isEdgeElement(sourceMagnet)) {
return new Rectangle(this.sourceAnchor.x, this.sourceAnchor.y)
}
return sourceView.getBBoxOfElement(sourceMagnet || sourceView.container)
}
get targetBBox() {
const targetView = this.targetView
if (!targetView) {
const targetDef = this.cell.getTarget() as Edge.TerminalPointData
return new Rectangle(targetDef.x, targetDef.y)
}
const targetMagnet = this.targetMagnet
if (targetView.isEdgeElement(targetMagnet)) {
return new Rectangle(this.targetAnchor.x, this.targetAnchor.y)
}
return targetView.getBBoxOfElement(targetMagnet || targetView.container)
}
isEdgeView(): this is EdgeView {
return true
}
confirmUpdate(flag: number, options: any = {}) {
let ref = flag
if (this.hasAction(ref, 'source')) {
if (!this.updateTerminalProperties('source')) {
return ref
}
ref = this.removeAction(ref, 'source')
}
if (this.hasAction(ref, 'target')) {
if (!this.updateTerminalProperties('target')) {
return ref
}
ref = this.removeAction(ref, 'target')
}
const graph = this.graph
const sourceView = this.sourceView
const targetView = this.targetView
if (
graph &&
((sourceView && !graph.renderer.isViewMounted(sourceView)) ||
(targetView && !graph.renderer.isViewMounted(targetView)))
) {
// Wait for the sourceView and targetView to be rendered.
return ref
}
if (this.hasAction(ref, 'render')) {
this.render()
ref = this.removeAction(ref, [
'render',
'update',
'vertices',
'labels',
'tools',
'widget',
])
return ref
}
ref = this.handleAction(ref, 'vertices', () => this.renderVertexMarkers())
ref = this.handleAction(ref, 'update', () => this.update(null, options))
ref = this.handleAction(ref, 'labels', () => this.onLabelsChange(options))
ref = this.handleAction(ref, 'tools', () => {
this.renderTools()
this.updateToolsPosition()
})
ref = this.handleAction(ref, 'widget', () => this.renderExternalTools())
return ref
}
onLabelsChange(options: any = {}) {
// Note: this optimization works in async=false mode only
if (this.shouldRerenderLabels(options)) {
this.renderLabels()
} else {
this.updateLabels()
}
this.updateLabelPositions()
}
protected shouldRerenderLabels(options: any = {}) {
const previousLabels = this.cell.previous('labels')
if (previousLabels == null) {
return true
}
// Here is an optimization for cases when we know, that change does
// not require re-rendering of all labels.
if ('propertyPathArray' in options && 'propertyValue' in options) {
// The label is setting by `prop()` method
const pathArray = options.propertyPathArray || []
const pathLength = pathArray.length
if (pathLength > 1) {
// We are changing a single label here e.g. 'labels/0/position'
const index = pathArray[1]
if (previousLabels[index]) {
if (pathLength === 2) {
// We are changing the entire label. Need to check if the
// markup is also being changed.
return (
typeof options.propertyValue === 'object' &&
ObjectExt.has(options.propertyValue, 'markup')
)
}
// We are changing a label property but not the markup
if (pathArray[2] !== 'markup') {
return false
}
}
}
}
return true
}
// #region render
protected containers: EdgeView.ContainerCache
protected labelCache: { [index: number]: Element }
protected labelSelectors: { [index: number]: Markup.Selectors }
render() {
this.empty()
this.containers = {}
this.renderMarkup()
this.renderLabels()
this.update()
return this
}
protected renderMarkup() {
const markup = this.cell.markup
if (markup) {
if (typeof markup === 'string') {
return this.renderStringMarkup(markup)
}
return this.renderJSONMarkup(markup)
}
throw new TypeError('Invalid edge markup.')
}
protected renderJSONMarkup(markup: Markup.JSONMarkup | Markup.JSONMarkup[]) {
const ret = this.parseJSONMarkup(markup, this.container)
this.selectors = ret.selectors
this.container.append(ret.fragment)
}
protected renderStringMarkup(markup: string) {
const cache = this.containers
const children = Vector.createVectors(markup)
// Cache children elements for quicker access.
children.forEach((child) => {
const className = child.attr('class')
if (className) {
cache[StringExt.camelCase(className) as keyof EdgeView.ContainerCache] =
child.node
}
})
this.renderTools()
this.renderVertexMarkers()
this.renderArrowheadMarkers()
Dom.append(
this.container,
children.map((child) => child.node),
)
}
protected renderLabels() {
const edge = this.cell
const labels = edge.getLabels()
const count = labels.length
let container = this.containers.labels
this.labelCache = {}
this.labelSelectors = {}
if (count <= 0) {
if (container && container.parentNode) {
container.parentNode.removeChild(container)
}
return this
}
if (container) {
this.empty(container)
} else {
container = Dom.createSvgElement('g')
this.addClass(this.prefixClassName('edge-labels'), container)
this.containers.labels = container
}
for (let i = 0, ii = labels.length; i < ii; i += 1) {
const label = labels[i]
const normalized = this.normalizeLabelMarkup(
this.parseLabelMarkup(label.markup),
)
let labelNode
let selectors
if (normalized) {
labelNode = normalized.node
selectors = normalized.selectors
} else {
const defaultLabel = edge.getDefaultLabel()
const normalized = this.normalizeLabelMarkup(
this.parseLabelMarkup(defaultLabel.markup),
)!
labelNode = normalized.node
selectors = normalized.selectors
}
labelNode.setAttribute('data-index', `${i}`)
container.appendChild(labelNode)
const rootSelector = this.rootSelector
if (selectors[rootSelector]) {
throw new Error('Ambiguous label root selector.')
}
selectors[rootSelector] = labelNode
this.labelCache[i] = labelNode
this.labelSelectors[i] = selectors
}
if (container.parentNode == null) {
this.container.appendChild(container)
}
this.updateLabels()
this.customizeLabels()
return this
}
protected parseLabelMarkup(markup?: Markup) {
if (markup) {
if (typeof markup === 'string') {
return this.parseLabelStringMarkup(markup)
}
return this.parseJSONMarkup(markup)
}
return null
}
protected parseLabelStringMarkup(labelMarkup: string) {
const children = Vector.createVectors(labelMarkup)
const fragment = document.createDocumentFragment()
for (let i = 0, n = children.length; i < n; i += 1) {
const currentChild = children[i].node
fragment.appendChild(currentChild)
}
return { fragment, selectors: {} }
}
protected normalizeLabelMarkup(
markup?: {
fragment: DocumentFragment
selectors: Markup.Selectors
} | null,
) {
if (markup == null) {
return
}
const fragment = markup.fragment
if (!(fragment instanceof DocumentFragment) || !fragment.hasChildNodes()) {
throw new Error('Invalid label markup.')
}
let vel
const childNodes = fragment.childNodes
if (childNodes.length > 1 || childNodes[0].nodeName.toUpperCase() !== 'G') {
// default markup fragment is not wrapped in `<g/>`
// add a `<g/>` container
vel = Vector.create('g').append(fragment)
} else {
vel = Vector.create(childNodes[0] as SVGElement)
}
vel.addClass(this.prefixClassName('edge-label'))
return {
node: vel.node,
selectors: markup.selectors,
}
}
protected updateLabels() {
if (this.containers.labels) {
const edge = this.cell
const labels = edge.labels
const canLabelMove = this.can('edgeLabelMovable')
const defaultLabel = edge.getDefaultLabel()
for (let i = 0, n = labels.length; i < n; i += 1) {
const elem = this.labelCache[i]
const selectors = this.labelSelectors[i]
elem.setAttribute('cursor', canLabelMove ? 'move' : 'default')
const label = labels[i]
const attrs = ObjectExt.merge({}, defaultLabel.attrs, label.attrs)
this.updateAttrs(elem, attrs, {
selectors,
rootBBox: label.size ? Rectangle.fromSize(label.size) : undefined,
})
}
}
}
protected mergeLabelAttrs(
hasCustomMarkup: boolean,
labelAttrs?: Attr.CellAttrs | null,
defaultLabelAttrs?: Attr.CellAttrs | null,
) {
if (labelAttrs === null) {
return null
}
if (labelAttrs === undefined) {
if (defaultLabelAttrs === null) {
return null
}
if (defaultLabelAttrs === undefined) {
return undefined
}
if (hasCustomMarkup) {
return defaultLabelAttrs
}
return ObjectExt.merge({}, defaultLabelAttrs)
}
if (hasCustomMarkup) {
return ObjectExt.merge({}, defaultLabelAttrs, labelAttrs)
}
}
protected customizeLabels() {
if (this.containers.labels) {
const edge = this.cell
const labels = edge.labels
for (let i = 0, n = labels.length; i < n; i += 1) {
const label = labels[i]
const container = this.labelCache[i]
const selectors = this.labelSelectors[i]
this.graph.hook.onEdgeLabelRendered({
edge,
label,
container,
selectors,
})
}
}
}
protected renderTools() {
const container = this.containers.tools
if (container == null) {
return this
}
const markup = this.cell.toolMarkup
const $container = this.$(container).empty()
if (Markup.isStringMarkup(markup)) {
let template = StringExt.template(markup)
const tool = Vector.create(template())
$container.append(tool.node)
this.toolCache = tool.node
// If `doubleTools` is enabled, we render copy of the tools on the
// other side of the edge as well but only if the edge is longer
// than `longLength`.
if (this.options.doubleTools) {
let tool2
const doubleToolMarkup = this.cell.doubleToolMarkup
if (Markup.isStringMarkup(doubleToolMarkup)) {
template = StringExt.template(doubleToolMarkup)
tool2 = Vector.create(template())
} else {
tool2 = tool.clone()
}
$container.append(tool2.node)
this.tool2Cache = tool2.node
}
}
return this
}
protected renderExternalTools() {
const tools = this.cell.getTools()
this.addTools(tools as ToolsView.Options)
return this
}
renderVertexMarkers() {
const container = this.containers.vertices
if (container == null) {
return this
}
const markup = this.cell.vertexMarkup
const $container = this.$(container).empty()
if (Markup.isStringMarkup(markup)) {
const template = StringExt.template(markup)
this.cell.getVertices().forEach((vertex, index) => {
$container.append(Vector.create(template({ index, ...vertex })).node)
})
}
return this
}
renderArrowheadMarkers() {
const container = this.containers.arrowheads
if (container == null) {
return this
}
const markup = this.cell.arrowheadMarkup
const $container = this.$(container).empty()
if (Markup.isStringMarkup(markup)) {
const template = StringExt.template(markup)
const sourceArrowhead = Vector.create(template({ end: 'source' })).node
const targetArrowhead = Vector.create(template({ end: 'target' })).node
this.containers.sourceArrowhead = sourceArrowhead
this.containers.targetArrowhead = targetArrowhead
$container.append(sourceArrowhead, targetArrowhead)
}
return this
}
// #endregion
// #region updating
update(partialAttrs?: Attr.CellAttrs | null, options: any = {}) {
this.cleanCache()
this.updateConnection(options)
const attrs = this.cell.getAttrs()
if (attrs != null) {
this.updateAttrs(this.container, attrs, {
attrs: partialAttrs === attrs ? null : partialAttrs,
selectors: this.selectors,
})
}
this.updateConnectionPath()
this.updateLabelPositions()
this.updateToolsPosition()
this.updateArrowheadMarkers()
if (options.toolId == null) {
this.renderExternalTools()
} else {
this.updateTools(options)
}
return this
}
removeRedundantLinearVertices(options: Edge.SetOptions = {}) {
const edge = this.cell
const vertices = edge.getVertices()
const routePoints = [this.sourceAnchor, ...vertices, this.targetAnchor]
const rawCount = routePoints.length
// Puts the route points into a polyline and try to simplify.
const polyline = new Polyline(routePoints)
polyline.simplify({ threshold: 0.01 })
const simplifiedPoints = polyline.points.map((point) => point.toJSON())
const simplifiedCount = simplifiedPoints.length
// If simplification did not remove any redundant vertices.
if (rawCount === simplifiedCount) {
return 0
}
// Sets simplified polyline points as edge vertices.
// Removes first and last polyline points again (source/target anchors).
edge.setVertices(simplifiedPoints.slice(1, simplifiedCount - 1), options)
return rawCount - simplifiedCount
}
updateConnectionPath() {
const containers = this.containers
if (containers.connection) {
const pathData = this.getConnectionPathData()
containers.connection.setAttribute('d', pathData)
}
if (containers.connectionWrap) {
const pathData = this.getConnectionPathData()
containers.connectionWrap.setAttribute('d', pathData)
}
if (containers.sourceMarker && containers.targetMarker) {
this.translateAndAutoOrientArrows(
containers.sourceMarker,
containers.targetMarker,
)
}
}
getTerminalView(type: Edge.TerminalType) {
switch (type) {
case 'source':
return this.sourceView || null
case 'target':
return this.targetView || null
default:
throw new Error(`Unknown terminal type '${type}'`)
}
}
getTerminalAnchor(type: Edge.TerminalType) {
switch (type) {
case 'source':
return Point.create(this.sourceAnchor)
case 'target':
return Point.create(this.targetAnchor)
default:
throw new Error(`Unknown terminal type '${type}'`)
}
}
getTerminalConnectionPoint(type: Edge.TerminalType) {
switch (type) {
case 'source':
return Point.create(this.sourcePoint)
case 'target':
return Point.create(this.targetPoint)
default:
throw new Error(`Unknown terminal type '${type}'`)
}
}
getTerminalMagnet(type: Edge.TerminalType, options: { raw?: boolean } = {}) {
switch (type) {
case 'source': {
if (options.raw) {
return this.sourceMagnet
}
const sourceView = this.sourceView
if (!sourceView) {
return null
}
return this.sourceMagnet || sourceView.container
}
case 'target': {
if (options.raw) {
return this.targetMagnet
}
const targetView = this.targetView
if (!targetView) {
return null
}
return this.targetMagnet || targetView.container
}
default: {
throw new Error(`Unknown terminal type '${type}'`)
}
}
}
updateConnection(options: any = {}) {
const edge = this.cell
// The edge is being translated by an ancestor that will shift
// source, target and vertices by an equal distance.
if (
options.translateBy &&
edge.isFragmentDescendantOf(options.translateBy)
) {
const tx = options.tx || 0
const ty = options.ty || 0
this.routePoints = new Polyline(this.routePoints).translate(tx, ty).points
this.translateConnectionPoints(tx, ty)
this.path.translate(tx, ty)
} else {
const vertices = edge.getVertices()
// 1. Find anchor points
const anchors = this.findAnchors(vertices)
this.sourceAnchor = anchors.source
this.targetAnchor = anchors.target
// 2. Find route points
this.routePoints = this.findRoutePoints(vertices)
// 3. Find connection points
const connectionPoints = this.findConnectionPoints(
this.routePoints,
this.sourceAnchor,
this.targetAnchor,
)
this.sourcePoint = connectionPoints.source
this.targetPoint = connectionPoints.target
// 4. Find Marker Connection Point
const markerPoints = this.findMarkerPoints(
this.routePoints,
this.sourcePoint,
this.targetPoint,
)
// 5. Make path
this.path = this.findPath(
this.routePoints,
markerPoints.source || this.sourcePoint,
markerPoints.target || this.targetPoint,
)
}
this.cleanCache()
}
protected findAnchors(vertices: Point.PointLike[]) {
const edge = this.cell
const source = edge.source as Edge.TerminalCellData
const target = edge.target as Edge.TerminalCellData
const firstVertex = vertices[0]
const lastVertex = vertices[vertices.length - 1]
if (target.priority && !source.priority) {
// Reversed order
return this.findAnchorsOrdered(
'target',
lastVertex,
'source',
firstVertex,
)
}
// Usual order
return this.findAnchorsOrdered('source', firstVertex, 'target', lastVertex)
}
protected findAnchorsOrdered(
firstType: Edge.TerminalType,
firstPoint: Point.PointLike,
secondType: Edge.TerminalType,
secondPoint: Point.PointLike,
) {
let firstAnchor: Point
let secondAnchor: Point
const edge = this.cell
const firstTerminal = edge[firstType]
const secondTerminal = edge[secondType]
const firstView = this.getTerminalView(firstType)
const secondView = this.getTerminalView(secondType)
const firstMagnet = this.getTerminalMagnet(firstType)
const secondMagnet = this.getTerminalMagnet(secondType)
if (firstView) {
let firstRef
if (firstPoint) {
firstRef = Point.create(firstPoint)
} else if (secondView) {
firstRef = secondMagnet
} else {
firstRef = Point.create(secondTerminal as Edge.TerminalPointData)
}
firstAnchor = this.getAnchor(
(firstTerminal as Edge.SetCellTerminalArgs).anchor,
firstView,
firstMagnet,
firstRef,
firstType,
)
} else {
firstAnchor = Point.create(firstTerminal as Edge.TerminalPointData)
}
if (secondView) {
const secondRef = Point.create(secondPoint || firstAnchor)
secondAnchor = this.getAnchor(
(secondTerminal as Edge.SetCellTerminalArgs).anchor,
secondView,
secondMagnet,
secondRef,
secondType,
)
} else {
secondAnchor = Point.isPointLike(secondTerminal)
? Point.create(secondTerminal)
: new Point()
}
return {
[firstType]: firstAnchor,
[secondType]: secondAnchor,
}
}
protected getAnchor(
def: NodeAnchor.ManaualItem | string | undefined,
cellView: CellView,
magnet: Element | null,
ref: Point | Element | null,
terminalType: Edge.TerminalType,
): Point {
const isEdge = cellView.isEdgeElement(magnet)
const connecting = this.graph.options.connecting
let config = typeof def === 'string' ? { name: def } : def
if (!config) {
const defaults = isEdge
? (terminalType === 'source'
? connecting.sourceEdgeAnchor
: connecting.targetEdgeAnchor) || connecting.edgeAnchor
: (terminalType === 'source'
? connecting.sourceAnchor
: connecting.targetAnchor) || connecting.anchor
config = typeof defaults === 'string' ? { name: defaults } : defaults
}
if (!config) {
throw new Error(`Anchor should be specified.`)
}
let anchor
const name = config.name
if (isEdge) {
const fn = EdgeAnchor.registry.get(name)
if (typeof fn !== 'function') {
return EdgeAnchor.registry.onNotFound(name)
}
anchor = FunctionExt.call(
fn,
this,
cellView as EdgeView,
magnet as SVGElement,
ref as Point.PointLike,
config.args || {},
terminalType,
)
} else {
const fn = NodeAnchor.registry.get(name)
if (typeof fn !== 'function') {
return NodeAnchor.registry.onNotFound(name)
}
anchor = FunctionExt.call(
fn,
this,
cellView as NodeView,
magnet as SVGElement,
ref as Point.PointLike,
config.args || {},
terminalType,
)
}
return anchor ? anchor.round(this.POINT_ROUNDING) : new Point()
}
protected findRoutePoints(vertices: Point.PointLike[] = []): Point[] {
const defaultRouter =
this.graph.options.connecting.router || Router.presets.normal
const router = this.cell.getRouter() || defaultRouter
let routePoints
if (typeof router === 'function') {
routePoints = FunctionExt.call(
router as Router.Definition<any>,
this,
vertices,
{},
this,
)
} else {
const name = typeof router === 'string' ? router : router.name
const args = typeof router === 'string' ? {} : router.args || {}
const fn = name ? Router.registry.get(name) : Router.presets.normal
if (typeof fn !== 'function') {
return Router.registry.onNotFound(name!)
}
routePoints = FunctionExt.call(fn, this, vertices, args, this)
}
return routePoints == null
? vertices.map((p) => Point.create(p))
: routePoints.map((p) => Point.create(p))
}
protected findConnectionPoints(
routePoints: Point[],
sourceAnchor: Point,
targetAnchor: Point,
) {
const edge = this.cell
const connecting = this.graph.options.connecting
const sourceTerminal = edge.getSource()
const targetTerminal = edge.getTarget()
const sourceView = this.sourceView
const targetView = this.targetView
const firstRoutePoint = routePoints[0]
const lastRoutePoint = routePoints[routePoints.length - 1]
// source
let sourcePoint
if (sourceView && !sourceView.isEdgeElement(this.sourceMagnet)) {
const sourceMagnet = this.sourceMagnet || sourceView.container
const sourcePointRef = firstRoutePoint || targetAnchor
const sourceLine = new Line(sourcePointRef, sourceAnchor)
const connectionPointDef =
sourceTerminal.connectionPoint ||
connecting.sourceConnectionPoint ||
connecting.connectionPoint
sourcePoint = this.getConnectionPoint(
connectionPointDef,
sourceView,
sourceMagnet,
sourceLine,
'source',
)
} else {
sourcePoint = sourceAnchor
}
// target
let targetPoint
if (targetView && !targetView.isEdgeElement(this.targetMagnet)) {
const targetMagnet = this.targetMagnet || targetView.container
const targetConnectionPointDef =
targetTerminal.connectionPoint ||
connecting.targetConnectionPoint ||
connecting.connectionPoint
const targetPointRef = lastRoutePoint || sourceAnchor
const targetLine = new Line(targetPointRef, targetAnchor)
targetPoint = this.getConnectionPoint(
targetConnectionPointDef,
targetView,
targetMagnet,
targetLine,
'target',
)
} else {
targetPoint = targetAnchor
}
return {
source: sourcePoint,
target: targetPoint,
}
}
protected getConnectionPoint(
def: string | ConnectionPoint.ManaualItem | undefined,
view: CellView,
magnet: Element,
line: Line,
endType: Edge.TerminalType,
) {
const anchor = line.end
if (def == null) {
return anchor
}
const name = typeof def === 'string' ? def : def.name
const args = typeof def === 'string' ? {} : def.args
const fn = ConnectionPoint.registry.get(name)
if (typeof fn !== 'function') {
return ConnectionPoint.registry.onNotFound(name)
}
const connectionPoint = FunctionExt.call(
fn,
this,
line,
view,
magnet as SVGElement,
args || {},
endType,
)
return connectionPoint ? connectionPoint.round(this.POINT_ROUNDING) : anchor
}
protected updateMarkerAttr(type: Edge.TerminalType) {
const attrs = this.cell.getAttrs()
const key = `.${type}-marker`
const partial = attrs && attrs[key]
if (partial) {
this.updateAttrs(
this.container,
{},
{
attrs: { [key]: partial },
selectors: this.selectors,
},
)
}
}
protected findMarkerPoints(
routePoints: Point[],
sourcePoint: Point,
targetPoint: Point,
) {
const getLineWidth = (type: Edge.TerminalType) => {
const attrs = this.cell.getAttrs()
const keys = Object.keys(attrs)
for (let i = 0, l = keys.length; i < l; i += 1) {
const attr = attrs[keys[i]]
if (attr[`${type}Marker`] || attr[`${type}-marker`]) {
const strokeWidth =
(attr.strokeWidth as string) || (attr['stroke-width'] as string)
if (strokeWidth) {
return parseFloat(strokeWidth)
}
break
}
}
return null
}
const firstRoutePoint = routePoints[0]
const lastRoutePoint = routePoints[routePoints.length - 1]
const sourceMarkerElem = this.containers.sourceMarker as SVGElement
const targetMarkerElem = this.containers.targetMarker as SVGElement
const cache = this.markerCache
let sourceMarkerPoint
let targetMarkerPoint
// Move the source point by the width of the marker taking into
// account its scale around x-axis. Note that scale is the only
// transform that makes sense to be set in `.marker-source`
// attributes object as all other transforms (translate/rotate)
// will be replaced by the `translateAndAutoOrient()` function.
if (sourceMarkerElem) {
this.updateMarkerAttr('source')
// support marker connection point registry???
cache.sourceBBox = cache.sourceBBox || Dom.getBBox(sourceMarkerElem)
if (cache.sourceBBox.width > 0) {
const scale = Dom.scale(sourceMarkerElem)
sourceMarkerPoint = sourcePoint
.clone()
.move(
firstRoutePoint || targetPoint,
cache.sourceBBox.width * scale.sx * -1,
)
}
} else {
const strokeWidth = getLineWidth('source')
if (strokeWidth) {
sourceMarkerPoint = sourcePoint
.clone()
.move(firstRoutePoint || targetPoint, -strokeWidth)
}
}
if (targetMarkerElem) {
this.updateMarkerAttr('target')
cache.targetBBox = cache.targetBBox || Dom.getBBox(targetMarkerElem)
if (cache.targetBBox.width > 0) {
const scale = Dom.scale(targetMarkerElem)
targetMarkerPoint = targetPoint
.clone()
.move(
lastRoutePoint || sourcePoint,
cache.targetBBox.width * scale.sx * -1,
)
}
} else {
const strokeWidth = getLineWidth('target')
if (strokeWidth) {
targetMarkerPoint = targetPoint
.clone()
.move(lastRoutePoint || sourcePoint, -strokeWidth)
}
}
// If there was no markup for the marker, use the connection point.
cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone()
cache.targetPoint = targetMarkerPoint || targetPoint.clone()
return {
source: sourceMarkerPoint,
target: targetMarkerPoint,
}
}
protected findPath(
routePoints: Point[],
sourcePoint: Point,
targetPoint: Point,
): Path {
const def =
this.cell.getConnector() || this.graph.options.connecting.connector
let name: string | undefined
let args: Connector.BaseOptions | undefined
let fn: Connector.Definition
if (typeof def === 'string') {
name = def
} else {
name = def.name
args = def.args
}
if (name) {
const method = Connector.registry.get(name)
if (typeof method !== 'function') {
return Connector.registry.onNotFound(name)
}
fn = method
} else {
fn = Connector.presets.normal
}
const path = FunctionExt.call(
fn,
this,
sourcePoint,
targetPoint,
routePoints,
{ ...args, raw: true },
this,
)
return typeof path === 'string' ? Path.parse(path) : path
}
protected translateConnectionPoints(tx: number, ty: number) {
const cache = this.markerCache
if (cache.sourcePoint) {
cache.sourcePoint.translate(tx, ty)
}
if (cache.targetPoint) {
cache.targetPoint.translate(tx, ty)
}
this.sourcePoint.translate(tx, ty)
this.targetPoint.translate(tx, ty)
this.sourceAnchor.translate(tx, ty)
this.targetAnchor.translate(tx, ty)
}
updateLabelPositions() {
if (this.containers.labels == null) {
return this
}
const path = this.path
if (!path) {
return this
}
const edge = this.cell
const labels = edge.getLabels()
if (labels.length === 0) {
return this
}
const defaultLabel = edge.getDefaultLabel()
const defaultPosition = this.normalizeLabelPosition(
defaultLabel.position as Edge.LabelPosition,
)
for (let i = 0, ii = labels.length; i < ii; i += 1) {
const label = labels[i]
const labelPosition = this.normalizeLabelPosition(
label.position as Edge.LabelPosition,
)
const pos = ObjectExt.merge({}, defaultPosition, labelPosition)
const matrix = this.getLabelTransformationMatrix(pos)
this.labelCache[i].setAttribute(
'transform',
Dom.matrixToTransformString(matrix),
)
}
return this
}
updateToolsPosition() {
if (this.containers.tools == null) {
return this
}
// Move the tools a bit to the target position but don't cover the
// `sourceArrowhead` marker. Note that the offset is hardcoded here.
// The offset should be always more than the
// `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking
// this up all the time would be slow.
let scale = ''
let offset = this.options.toolsOffset
const connectionLength = this.getConnectionLength()
// Firefox returns `connectionLength=NaN` in odd cases (for bezier curves).
// In that case we won't update tools position at all.
if (connectionLength != null) {
// If the edge is too short, make the tools half the
// size and the offset twice as low.
if (connectionLength < this.options.shortLength) {
scale = 'scale(.5)'
offset /= 2
}
let pos = this.getPointAtLength(offset)
if (pos != null) {
Dom.attr(
this.toolCache,
'transform',
`translate(${pos.x},${pos.y}) ${scale}`,
)
}
if (
this.options.doubleTools &&
connectionLength >= this.options.longLength
) {
const doubleToolsOffset = this.options.doubleToolsOffset || offset
pos = this.getPointAtLength(connectionLength - doubleToolsOffset)
if (pos != null) {
Dom.attr(
this.tool2Cache,
'transform',
`translate(${pos.x},${pos.y}) ${scale}`,
)
}
Dom.attr(this.tool2Cache, 'visibility', 'visible')
} else if (this.options.doubleTools) {
Dom.attr(this.tool2Cache, 'visibility', 'hidden')
}
}
return this
}
updateArrowheadMarkers() {
const container = this.containers.arrowheads
if (container == null) {
return this
}
if ((container as HTMLElement).style.display === 'none') {
return this
}
const sourceArrowhead = this.containers.sourceArrowhead
const targetArrowhead = this.containers.targetArrowhead
if (sourceArrowhead && targetArrowhead) {
const len = this.getConnectionLength() || 0
const sx = len < this.options.shortLength ? 0.5 : 1
Dom.scale(sourceArrowhead as SVGElement, sx)
Dom.scale(targetArrowhead as SVGElement, sx)
this.translateAndAutoOrientArrows(sourceArrowhead, targetArrowhead)
}
return this
}
updateTerminalProperties(type: Edge.TerminalType) {
const edge = this.cell
const graph = this.graph
const terminal = edge[type]
const nodeId = terminal && (terminal as Edge.TerminalCellData).cell
const viewKey = `${type}View` as 'sourceView' | 'targetView'
// terminal is a point
if (!nodeId) {
this[viewKey] = null
this.updateTerminalMagnet(type)
return true
}
const terminalCell = graph.getCellById(nodeId)
if (!terminalCell) {
throw new Error(`Edge's ${type} node with id "${nodeId}" not exists`)
}
const endView = terminalCell.findView(graph)
if (!endView) {
return false
}
this[viewKey] = endView
this.updateTerminalMagnet(type)
return true
}
updateTerminalMagnet(type: Edge.TerminalType) {
const propName = `${type}Magnet` as 'sourceMagnet' | 'targetMagnet'
const terminalView = this.getTerminalView(type)
if (terminalView) {
let magnet = terminalView.getMagnetFromEdgeTerminal(this.cell[type])
if (magnet === terminalView.container) {
magnet = null
}
this[propName] = magnet
} else {
this[propName] = null
}
}
protected translateAndAutoOrientArrows(
sourceArrow?: Element,
targetArrow?: Element,
) {
const route = this.routePoints
if (sourceArrow) {
Dom.translateAndAutoOrient(
sourceArrow as SVGElement,
this.sourcePoint,
route[0] || this.targetPoint,
this.graph.view.stage,
)
}
if (targetArrow) {
Dom.translateAndAutoOrient(
targetArrow as SVGElement,
this.targetPoint,
route[route.length - 1] || this.sourcePoint,
this.graph.view.stage,
)
}
}
protected getLabelPositionAngle(idx: number) {
const label = this.cell.getLabelAt(idx)
if (label && label.position && typeof label.position === 'object') {
return label.position.angle || 0
}
return 0
}
protected getLabelPositionArgs(idx: number) {
const label = this.cell.getLabelAt(idx)
if (label && label.position && typeof label.position === 'object') {
return label.position.options
}
}
protected getDefaultLabelPositionArgs() {
const defaultLabel = this.cell.getDefaultLabel()
if (
defaultLabel &&
defaultLabel.position &&
typeof defaultLabel.position === 'object'
) {
return defaultLabel.position.options
}
}
// merge default label position args into label position args
// keep `undefined` or `null` because `{}` means something else
protected mergeLabelPositionArgs(
labelPositionArgs?: Edge.LabelPositionOptions,
defaultLabelPositionArgs?: Edge.LabelPositionOptions,
) {
if (labelPositionArgs === null) {
return null
}
if (labelPositionArgs === undefined) {
if (defaultLabelPositionArgs === null) {
return null
}
return defaultLabelPositionArgs
}
return ObjectExt.merge({}, defaultLabelPositionArgs, labelPositionArgs)
}
// Add default label at given position at end of `labels` array.
addLabel(
x: number,
y: number,
options?: Edge.LabelPositionOptions & Edge.SetOptions,
): number
addLabel(
x: number,
y: number,
angle: number,
options?: Edge.LabelPositionOptions & Edge.SetOptions,
): number
addLabel(
p: Point | Point.PointLike,
options?: Edge.LabelPositionOptions & Edge.SetOptions,
): number
addLabel(
p: Point | Point.PointLike,
angle: number,
options?: Edge.LabelPositionOptions & Edge.SetOptions,
): number
addLabel(
p1: number | Point | Point.PointLike,
p2?: number | (Edge.LabelPositionOptions & Edge.SetOptions),
p3?: number | (Edge.LabelPositionOptions & Edge.SetOptions),
options?: Edge.LabelPositionOptions & Edge.SetOptions,
) {
let localX: number
let localY: number
let localAngle = 0
let localOptions
if (typeof p1 !== 'number') {
localX = p1.x
localY = p1.y
if (typeof p2 === 'number') {
localAngle = p2
localOptions = p3
} else {
localOptions = p2
}
} else {
localX = p1
localY = p2 as number
if (typeof p3 === 'number') {
localAngle = p3
localOptions = options
} else {
localOptions = p3
}
}
// merge label position arguments
const defaultLabelPositionArgs = this.getDefaultLabelPositionArgs()
const labelPositionArgs = localOptions as Edge.LabelPositionOptions
const positionArgs = this.mergeLabelPositionArgs(
labelPositionArgs,
defaultLabelPositionArgs,
)
// append label to labels array
const label = {
position: this.getLabelPosition(localX, localY, localAngle, positionArgs),
}
const index = -1
this.cell.insertLabel(label, index, localOptions as Edge.SetOptions)
return index
}
addVertex(p: Point | Point.PointLike, options?: Edge.SetOptions): number
addVertex(x: number, y: number, options?: Edge.SetOptions): number
addVertex(
x: number | Point | Point.PointLike,
y?: number | Edge.SetOptions,
options?: Edge.SetOptions,
) {
const isPoint = typeof x !== 'number'
const localX = isPoint ? (x as Point).x : (x as number)
const localY = isPoint ? (x as Point).y : (y as number)
const localOptions = isPoint ? (y as Edge.SetOptions) : options
const vertex = { x: localX, y: localY }
const index = this.getVertexIndex(localX, localY)
this.cell.insertVertex(vertex, index, localOptions)
return index
}
sendToken(
token: string | SVGElement,
options?:
| number
| ({
duration?: number
reversed?: boolean
selector?: string
// https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/rotate
rotate?: boolean | number | 'auto' | 'auto-reverse'
// https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/calcMode
timing?: 'linear' | 'discrete' | 'paced' | 'spline'
} & Dom.AnimateCallbacks &
KeyValue<string | number | undefined>),
callback?: () => void,
) {
let duration
let reversed
let selector
let rorate
let timing = 'linear'
if (typeof options === 'object') {
duration = options.duration
reversed = options.reversed === true
selector = options.selector
if (options.rotate === false) {
rorate = ''
} else if (options.rotate === true) {
rorate = 'auto'
} else if (options.rotate != null) {
rorate = `${options.rotate}`
}
if (options.timing) {
timing = options.timing
}
} else {
duration = options
reversed = false
selector = null
}
duration = duration || 1000
const attrs: Dom.AnimationOptions = {
dur: `${duration}ms`,
repeatCount: '1',
calcMode: timing,
fill: 'freeze',
}
if (rorate) {
attrs.rotate = rorate
}
if (reversed) {
attrs.keyPoints = '1;0'
attrs.keyTimes = '0;1'
}
if (typeof options === 'object') {
const { duration, reversed, selector, rotate, timing, ...others } =
options
Object.keys(others).forEach((key) => {
attrs[key] = others[key]
})
}
let path
if (typeof selector === 'string') {
path = this.findOne(selector, this.container, this.selectors)
} else {
// Select connection path automatically.
path = this.containers.connection
? this.containers.connection
: this.container.querySelector('path')
}
if (!(path instanceof SVGPathElement)) {
throw new Error('Token animation requires a valid connection path.')
}
const target = typeof token === 'string' ? this.findOne(token) : token
if (target == null) {
throw new Error('Token animation requires a valid token element.')
}
const parent = target.parentNode
const revert = () => {
if (!parent) {
Dom.remove(target)
}
}
const vToken = Vector.create(target as SVGElement)
if (!parent) {
vToken.appendTo(this.graph.view.stage)
}
const onComplete = attrs.complete
attrs.complete = (e: Event) => {
revert()
if (callback) {
callback()
}
if (onComplete) {
onComplete(e)
}
}
const stop = vToken.animateAlongPath(attrs, path)
return () => {
revert()
stop()
}
}
// #endregion
getConnection() {
return this.path != null ? this.path.clone() : null
}
getConnectionPathData() {
if (this.path == null) {
return ''
}
const cache = this.cache.pathCache
if (!ObjectExt.has(cache, 'data')) {
cache.data = this.path.serialize()
}
return cache.data || ''
}
getConnectionSubdivisions() {
if (this.path == null) {
return null
}
const cache = this.cache.pathCache
if (!ObjectExt.has(cache, 'segmentSubdivisions')) {
cache.segmentSubdivisions = this.path.getSegmentSubdivisions()
}
return cache.segmentSubdivisions
}
getConnectionLength() {
if (this.path == null) {
return 0
}
const cache = this.cache.pathCache
if (!ObjectExt.has(cache, 'length')) {
cache.length = this.path.length({
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
return cache.length
}
getPointAtLength(length: number) {
if (this.path == null) {
return null
}
return this.path.pointAtLength(length, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
getPointAtRatio(ratio: number) {
if (this.path == null) {
return null
}
if (NumberExt.isPercentage(ratio)) {
// eslint-disable-next-line
ratio = parseFloat(ratio) / 100
}
return this.path.pointAt(ratio, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
getTangentAtLength(length: number) {
if (this.path == null) {
return null
}
return this.path.tangentAtLength(length, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
getTangentAtRatio(ratio: number) {
if (this.path == null) {
return null
}
return this.path.tangentAt(ratio, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
getClosestPoint(point: Point.PointLike) {
if (this.path == null) {
return null
}
return this.path.closestPoint(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
getClosestPointLength(point: Point.PointLike) {
if (this.path == null) {
return null
}
return this.path.closestPointLength(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
getClosestPointRatio(point: Point.PointLike) {
if (this.path == null) {
return null
}
return this.path.closestPointNormalizedLength(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
})
}
// Get label position object based on two provided coordinates, x and y.
getLabelPosition(
x: number,
y: number,
options?: Edge.LabelPositionOptions | null,
): Edge.LabelPositionObject
getLabelPosition(
x: number,
y: number,
angle: number,
options?: Edge.LabelPositionOptions | null,
): Edge.LabelPositionObject
getLabelPosition(
x: number,
y: number,
p3?: number | Edge.LabelPositionOptions | null,
p4?: Edge.LabelPositionOptions | null,
): Edge.LabelPositionObject {
const pos: Edge.LabelPositionObject = { distance: 0 }
// normalize data from the two possible signatures
let angle = 0
let options
if (typeof p3 === 'number') {
angle = p3
options = p4
} else {
options = p3
}
if (options != null) {
pos.options = options
}
// identify distance/offset settings
const isOffsetAbsolute = options && options.absoluteOffset
const isDistanceRelative = !(options && options.absoluteDistance)
const isDistanceAbsoluteReverse =
options && options.absoluteDistance && options.reverseDistance
// find closest point t
const path = this.path
const pathOptions = {
segmentSubdivisions: this.getConnectionSubdivisions(),
}
const labelPoint = new Point(x, y)
const t = path.closestPointT(labelPoint, pathOptions)!
// distance
const totalLength = this.getConnectionLength() || 0
let labelDistance = path.lengthAtT(t, pathOptions)
if (isDistanceRelative) {
labelDistance = totalLength > 0 ? labelDistance / totalLength : 0
}
if (isDistanceAbsoluteReverse) {
// fix for end point (-0 => 1)
labelDistance = -1 * (totalLength - labelDistance) || 1
}
pos.distance = labelDistance
// offset
// use absolute offset if:
// - options.absoluteOffset is true,
// - options.absoluteOffset is not true but there is no tangent
let tangent
if (!isOffsetAbsolute) tangent = path.tangentAtT(t)
let labelOffset
if (tangent) {
labelOffset = tangent.pointOffset(labelPoint)
} else {
const closestPoint = path.pointAtT(t)!
const labelOffsetDiff = labelPoint.diff(closestPoint)
labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y }
}
pos.offset = labelOffset
pos.angle = angle
return pos
}
protected normalizeLabelPosition(): undefined
protected normalizeLabelPosition(
pos: Edge.LabelPosition,
): Edge.LabelPositionObject
protected normalizeLabelPosition(
pos?: Edge.LabelPosition,
): Edge.LabelPositionObject | undefined {
if (typeof pos === 'number') {
return { distance: pos }
}
return pos
}
protected getLabelTransformationMatrix(labelPosition: Edge.LabelPosition) {
const pos = this.normalizeLabelPosition(labelPosition)
const options = pos.options || {}
const labelAngle = pos.angle || 0
const labelDistance = pos.distance
const isDistanceRelative = labelDistance > 0 && labelDistance <= 1
let labelOffset = 0
const offsetCoord = { x: 0, y: 0 }
const offset = pos.offset
if (offset) {
if (typeof offset === 'number') {
labelOffset = offset
} else {
if (offset.x != null) {
offsetCoord.x = offset.x
}
if (offset.y != null) {
offsetCoord.y = offset.y
}
}
}
const isOffsetAbsolute =
offsetCoord.x !== 0 || offsetCoord.y !== 0 || labelOffset === 0
const isKeepGradient = options.keepGradient
const isEnsureLegibility = options.ensureLegibility
const path = this.path
const pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }
const distance = isDistanceRelative
? labelDistance * this.getConnectionLength()!
: labelDistance
const tangent = path.tangentAtLength(distance, pathOpt)
let translation
let angle = labelAngle
if (tangent) {
if (isOffsetAbsolute) {
translation = tangent.start
translation.translate(off