@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
521 lines (464 loc) • 14.6 kB
text/typescript
import { Dom, ObjectExt, FunctionExt } from '../../util'
import { Point, Line } from '../../geometry'
import { Graph } from '../../graph'
import { Edge } from '../../model/edge'
import { View } from '../../view/view'
import { CellView } from '../../view/cell'
import { EdgeView } from '../../view/edge'
import { ToolsView } from '../../view/tool'
import * as Util from './util'
import { Attr } from '../attr'
export class Segments extends ToolsView.ToolItem<EdgeView, Segments.Options> {
protected handles: Segments.Handle[] = []
protected get vertices() {
return this.cellView.cell.getVertices()
}
update() {
this.render()
return this
}
protected onRender() {
Dom.addClass(this.container, this.prefixClassName('edge-tool-segments'))
this.resetHandles()
const edgeView = this.cellView
const vertices = [...this.vertices]
vertices.unshift(edgeView.sourcePoint)
vertices.push(edgeView.targetPoint)
for (let i = 0, l = vertices.length; i < l - 1; i += 1) {
const vertex = vertices[i]
const nextVertex = vertices[i + 1]
const handle = this.renderHandle(vertex, nextVertex, i)
this.stamp(handle.container)
this.handles.push(handle)
}
return this
}
protected renderHandle(
vertex: Point.PointLike,
nextVertex: Point.PointLike,
index: number,
) {
const handle = this.options.createHandle!({
index,
graph: this.graph,
guard: (evt) => this.guard(evt),
attrs: this.options.attrs || {},
})
if (this.options.processHandle) {
this.options.processHandle(handle)
}
this.graph.hook.onToolItemCreated({
name: 'segments',
cell: this.cell,
view: this.cellView,
tool: handle,
})
this.updateHandle(handle, vertex, nextVertex)
this.container.appendChild(handle.container)
this.startHandleListening(handle)
return handle
}
protected startHandleListening(handle: Segments.Handle) {
handle.on('change', this.onHandleChange, this)
handle.on('changing', this.onHandleChanging, this)
handle.on('changed', this.onHandleChanged, this)
}
protected stopHandleListening(handle: Segments.Handle) {
handle.off('change', this.onHandleChange, this)
handle.off('changing', this.onHandleChanging, this)
handle.off('changed', this.onHandleChanged, this)
}
protected resetHandles() {
const handles = this.handles
this.handles = []
if (handles) {
handles.forEach((handle) => {
this.stopHandleListening(handle)
handle.remove()
})
}
}
protected shiftHandleIndexes(delta: number) {
const handles = this.handles
for (let i = 0, n = handles.length; i < n; i += 1) {
handles[i].options.index! += delta
}
}
protected resetAnchor(
type: Edge.TerminalType,
anchor: Edge.TerminalCellData['anchor'],
) {
const edge = this.cellView.cell
const options = {
ui: true,
toolId: this.cid,
}
if (anchor) {
edge.prop([type, 'anchor'], anchor, options)
} else {
edge.removeProp([type, 'anchor'], options)
}
}
protected snapHandle(
handle: Segments.Handle,
position: Point.PointLike,
data: Segments.EventData,
) {
const axis = handle.options.axis!
const index = handle.options.index!
const edgeView = this.cellView
const edge = edgeView.cell
const vertices = edge.getVertices()
const prev = vertices[index - 2] || data.sourceAnchor
const next = vertices[index + 1] || data.targetAnchor
const snapRadius = this.options.snapRadius
if (Math.abs(position[axis] - prev[axis]) < snapRadius) {
position[axis] = prev[axis]
} else if (Math.abs(position[axis] - next[axis]) < snapRadius) {
position[axis] = next[axis]
}
return position
}
protected onHandleChanging({
handle,
e,
}: Segments.Handle.EventArgs['changing']) {
const graph = this.graph
const options = this.options
const edgeView = this.cellView
const anchorFn = options.anchor
const axis = handle.options.axis!
const index = handle.options.index! - 1
const data = this.getEventData<Segments.EventData>(e)
const evt = this.normalizeEvent(e)
const coords = graph.snapToGrid(evt.clientX, evt.clientY)
const position = this.snapHandle(handle, coords.clone(), data)
const vertices = ObjectExt.cloneDeep(this.vertices)
let vertex = vertices[index]
let nextVertex = vertices[index + 1]
// First Segment
const sourceView = edgeView.sourceView
const sourceBBox = edgeView.sourceBBox
let changeSourceAnchor = false
let deleteSourceAnchor = false
if (!vertex) {
vertex = edgeView.sourceAnchor.toJSON()
vertex[axis] = position[axis]
if (sourceBBox.containsPoint(vertex)) {
changeSourceAnchor = true
} else {
vertices.unshift(vertex)
this.shiftHandleIndexes(1)
deleteSourceAnchor = true
}
} else if (index === 0) {
if (sourceBBox.containsPoint(vertex)) {
vertices.shift()
this.shiftHandleIndexes(-1)
changeSourceAnchor = true
} else {
vertex[axis] = position[axis]
deleteSourceAnchor = true
}
} else {
vertex[axis] = position[axis]
}
if (typeof anchorFn === 'function' && sourceView) {
if (changeSourceAnchor) {
const sourceAnchorPosition = data.sourceAnchor.clone()
sourceAnchorPosition[axis] = position[axis]
const sourceAnchor = FunctionExt.call(
anchorFn,
edgeView,
sourceAnchorPosition,
sourceView,
edgeView.sourceMagnet || sourceView.container,
'source',
edgeView,
this,
)
this.resetAnchor('source', sourceAnchor)
}
if (deleteSourceAnchor) {
this.resetAnchor('source', data.sourceAnchorDef)
}
}
// Last segment
const targetView = edgeView.targetView
const targetBBox = edgeView.targetBBox
let changeTargetAnchor = false
let deleteTargetAnchor = false
if (!nextVertex) {
nextVertex = edgeView.targetAnchor.toJSON()
nextVertex[axis] = position[axis]
if (targetBBox.containsPoint(nextVertex)) {
changeTargetAnchor = true
} else {
vertices.push(nextVertex)
deleteTargetAnchor = true
}
} else if (index === vertices.length - 2) {
if (targetBBox.containsPoint(nextVertex)) {
vertices.pop()
changeTargetAnchor = true
} else {
nextVertex[axis] = position[axis]
deleteTargetAnchor = true
}
} else {
nextVertex[axis] = position[axis]
}
if (typeof anchorFn === 'function' && targetView) {
if (changeTargetAnchor) {
const targetAnchorPosition = data.targetAnchor.clone()
targetAnchorPosition[axis] = position[axis]
const targetAnchor = FunctionExt.call(
anchorFn,
edgeView,
targetAnchorPosition,
targetView,
edgeView.targetMagnet || targetView.container,
'target',
edgeView,
this,
)
this.resetAnchor('target', targetAnchor)
}
if (deleteTargetAnchor) {
this.resetAnchor('target', data.targetAnchorDef)
}
}
if (!Point.equalPoints(vertices, this.vertices)) {
this.cellView.cell.setVertices(vertices, { ui: true, toolId: this.cid })
}
this.updateHandle(handle, vertex, nextVertex, 0)
if (!options.stopPropagation) {
edgeView.notifyMouseMove(evt, coords.x, coords.y)
}
}
protected onHandleChange({ handle, e }: Segments.Handle.EventArgs['change']) {
const options = this.options
const handles = this.handles
const edgeView = this.cellView
const index = handle.options.index
if (!Array.isArray(handles)) {
return
}
for (let i = 0, n = handles.length; i < n; i += 1) {
if (i !== index) {
handles[i].hide()
}
}
this.focus()
this.setEventData<Segments.EventData>(e, {
sourceAnchor: edgeView.sourceAnchor.clone(),
targetAnchor: edgeView.targetAnchor.clone(),
sourceAnchorDef: ObjectExt.cloneDeep(
this.cell.prop(['source', 'anchor']),
),
targetAnchorDef: ObjectExt.cloneDeep(
this.cell.prop(['target', 'anchor']),
),
})
this.cell.startBatch('move-segment', { ui: true, toolId: this.cid })
if (!options.stopPropagation) {
const normalizedEvent = this.normalizeEvent(e)
const coords = this.graph.snapToGrid(
normalizedEvent.clientX,
normalizedEvent.clientY,
)
edgeView.notifyMouseDown(normalizedEvent, coords.x, coords.y)
}
}
protected onHandleChanged({ e }: Segments.Handle.EventArgs['changed']) {
const options = this.options
const edgeView = this.cellView
if (options.removeRedundancies) {
edgeView.removeRedundantLinearVertices({ ui: true, toolId: this.cid })
}
const normalizedEvent = this.normalizeEvent(e)
const coords = this.graph.snapToGrid(
normalizedEvent.clientX,
normalizedEvent.clientY,
)
this.render()
this.blur()
this.cell.stopBatch('move-segment', { ui: true, toolId: this.cid })
if (!options.stopPropagation) {
edgeView.notifyMouseUp(normalizedEvent, coords.x, coords.y)
}
edgeView.checkMouseleave(normalizedEvent)
}
protected updateHandle(
handle: Segments.Handle,
vertex: Point.PointLike,
nextVertex: Point.PointLike,
offset = 0,
) {
const precision = this.options.precision || 0
const vertical = Math.abs(vertex.x - nextVertex.x) < precision
const horizontal = Math.abs(vertex.y - nextVertex.y) < precision
if (vertical || horizontal) {
const segmentLine = new Line(vertex, nextVertex)
const length = segmentLine.length()
if (length < this.options.threshold) {
handle.hide()
} else {
const position = segmentLine.getCenter()
const axis = vertical ? 'x' : 'y'
position[axis] += offset || 0
const angle = segmentLine.vector().vectorAngle(new Point(1, 0))
handle.updatePosition(position.x, position.y, angle, this.cellView)
handle.show()
handle.options.axis = axis
}
} else {
handle.hide()
}
}
protected onRemove() {
this.resetHandles()
}
}
export namespace Segments {
export interface Options extends ToolsView.ToolItem.Options {
threshold: number
precision?: number
snapRadius: number
stopPropagation: boolean
removeRedundancies: boolean
attrs: Attr.SimpleAttrs | ((handle: Handle) => Attr.SimpleAttrs)
anchor?: (
this: EdgeView,
pos: Point,
terminalView: CellView,
terminalMagnet: Element | null,
terminalType: Edge.TerminalType,
edgeView: EdgeView,
toolView: Segments,
) => Edge.TerminalCellData['anchor']
createHandle?: (options: Handle.Options) => Handle
processHandle?: (handle: Handle) => void
}
export interface EventData {
sourceAnchor: Point
targetAnchor: Point
sourceAnchorDef: Edge.TerminalCellData['anchor']
targetAnchorDef: Edge.TerminalCellData['anchor']
}
}
export namespace Segments {
export class Handle extends View<Handle.EventArgs> {
public container: SVGRectElement
constructor(public options: Handle.Options) {
super()
this.render()
this.delegateEvents({
mousedown: 'onMouseDown',
touchstart: 'onMouseDown',
})
}
render() {
this.container = View.createElement('rect', true) as SVGRectElement
const attrs = this.options.attrs
if (typeof attrs === 'function') {
const defaults = Segments.getDefaults<Segments.Options>()
this.setAttrs({
...defaults.attrs,
...attrs(this),
})
} else {
this.setAttrs(attrs)
}
this.addClass(this.prefixClassName('edge-tool-segment'))
}
updatePosition(x: number, y: number, angle: number, view: EdgeView) {
const p = view.getClosestPoint(new Point(x, y)) || new Point(x, y)
let matrix = Dom.createSVGMatrix().translate(p.x, p.y)
if (!p.equals({ x, y })) {
const line = new Line(x, y, p.x, p.y)
let deg = line.vector().vectorAngle(new Point(1, 0))
if (deg !== 0) {
deg += 90
}
matrix = matrix.rotate(deg)
} else {
matrix = matrix.rotate(angle)
}
this.setAttrs({
transform: Dom.matrixToTransformString(matrix),
cursor: angle % 180 === 0 ? 'row-resize' : 'col-resize',
})
}
protected onMouseDown(evt: JQuery.MouseDownEvent) {
if (this.options.guard(evt)) {
return
}
this.trigger('change', { e: evt, handle: this })
evt.stopPropagation()
evt.preventDefault()
this.options.graph.view.undelegateEvents()
this.delegateDocumentEvents(
{
mousemove: 'onMouseMove',
touchmove: 'onMouseMove',
mouseup: 'onMouseUp',
touchend: 'onMouseUp',
touchcancel: 'onMouseUp',
},
evt.data,
)
}
protected onMouseMove(evt: JQuery.MouseMoveEvent) {
this.emit('changing', { e: evt, handle: this })
}
protected onMouseUp(evt: JQuery.MouseUpEvent) {
this.emit('changed', { e: evt, handle: this })
this.undelegateDocumentEvents()
this.options.graph.view.delegateEvents()
}
show() {
this.container.style.display = ''
}
hide() {
this.container.style.display = 'none'
}
}
export namespace Handle {
export interface Options {
graph: Graph
guard: (evt: JQuery.TriggeredEvent) => boolean
attrs: Attr.SimpleAttrs | ((handle: Handle) => Attr.SimpleAttrs)
index?: number
axis?: 'x' | 'y'
}
export interface EventArgs {
change: { e: JQuery.MouseDownEvent; handle: Handle }
changing: { e: JQuery.MouseMoveEvent; handle: Handle }
changed: { e: JQuery.MouseUpEvent; handle: Handle }
}
}
}
export namespace Segments {
Segments.config<Options>({
name: 'segments',
precision: 0.5,
threshold: 40,
snapRadius: 10,
stopPropagation: true,
removeRedundancies: true,
attrs: {
width: 20,
height: 8,
x: -10,
y: -4,
rx: 4,
ry: 4,
fill: '#333',
stroke: '#fff',
'stroke-width': 2,
},
createHandle: (options) => new Handle(options),
anchor: Util.getAnchor,
})
}