UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

506 lines (445 loc) 13 kB
import { Util } from '../../global' import { StringExt, FunctionExt } from '../../util' import { Point, Rectangle, Angle } from '../../geometry' import { Cell } from '../../model/cell' import { Node } from '../../model/node' import { Edge } from '../../model/edge' import { CellView } from '../../view/cell' import { NodeView } from '../../view/node' import { EdgeView } from '../../view/edge' import { Handle } from '../common' import { notify } from '../transform/util' import { Halo } from './index' export class NodePreset { private edgeView: EdgeView | null private flip: number constructor(private halo: Halo) {} get options() { return this.halo.options } get graph() { return this.halo.graph } get model() { return this.halo.model } get view() { return this.halo.view } get cell() { return this.halo.cell } get node() { return this.cell as Node } getPresets(): Halo.Options { return { className: 'type-node', handles: [ { name: 'remove', position: 'nw', events: { mousedown: this.removeCell.bind(this), }, icon: null, }, { name: 'resize', position: 'se', events: { mousedown: this.startResize.bind(this), mousemove: this.doResize.bind(this), mouseup: this.stopResize.bind(this), }, icon: null, }, { name: 'clone', position: 'n', events: { mousedown: this.startClone.bind(this), mousemove: this.doClone.bind(this), mouseup: this.stopClone.bind(this), }, icon: null, }, { name: 'link', position: 'e', events: { mousedown: this.startLink.bind(this), mousemove: this.doLink.bind(this), mouseup: this.stopLink.bind(this), }, icon: null, }, { name: 'fork', position: 'ne', events: { mousedown: this.startFork.bind(this), mousemove: this.doFork.bind(this), mouseup: this.stopFork.bind(this), }, icon: null, }, { name: 'unlink', position: 'w', events: { mousedown: this.unlink.bind(this), }, icon: null, }, { name: 'rotate', position: 'sw', events: { mousedown: this.startRotate.bind(this), mousemove: this.doRotate.bind(this), mouseup: this.stopRotate.bind(this), }, icon: null, }, ], bbox(view) { if (this.options.useCellGeometry) { const node = view.cell as Node return node.getBBox() } return view.getBBox() }, content(view) { const template = StringExt.template( 'x: <%= x %>, y: <%= y %>, width: <%= width %>, height: <%= height %>, angle: <%= angle %>', ) const cell = view.cell as Node const bbox = cell.getBBox() return template({ x: Math.floor(bbox.x), y: Math.floor(bbox.y), width: Math.floor(bbox.width), height: Math.floor(bbox.height), angle: Math.floor(cell.getAngle()), }) }, magnet(view) { return view.container }, tinyThreshold: 40, smallThreshold: 80, loopEdgePreferredSide: 'top', loopEdgeWidth: 40, rotateGrid: 15, rotateEmbeds: false, } } removeCell() { this.model.removeConnectedEdges(this.cell) this.cell.remove() } // #region create edge startLink({ x, y }: Handle.EventArgs) { this.halo.startBatch() const graph = this.graph const edge = this.createEdgeConnectedToSource() edge.setTarget({ x, y }) this.model.addEdge(edge, { validation: false, halo: this.halo.cid, async: false, }) graph.view.undelegateEvents() this.edgeView = graph.renderer.findViewByCell(edge) as EdgeView this.edgeView.prepareArrowheadDragging('target', { x, y, fallbackAction: 'remove', }) } createEdgeConnectedToSource() { const magnet = this.getMagnet(this.view, 'source') const terminal = this.getEdgeTerminal(this.view, magnet) const edge = this.graph.hook.getDefaultEdge(this.view, magnet) edge.setSource(terminal) return edge } getMagnet(view: CellView, terminal: Edge.TerminalType) { const magnet = this.options.magnet if (typeof magnet === 'function') { const val = FunctionExt.call(magnet, this.halo, view, terminal) if (val instanceof SVGElement) { return val } } throw new Error('`magnet()` has to return an SVGElement') } getEdgeTerminal(view: CellView, magnet: Element) { const terminal: Edge.TerminalCellData = { cell: view.cell.id, } if (magnet !== view.container) { const port = magnet.getAttribute('port') if (port) { terminal.port = port } else { terminal.selector = view.getSelector(magnet) } } return terminal } doLink({ e, x, y }: Handle.EventArgs) { if (this.edgeView) { this.edgeView.onMouseMove(e as JQuery.MouseMoveEvent, x, y) } } stopLink({ e, x, y }: Handle.EventArgs) { const edgeView = this.edgeView if (edgeView) { edgeView.onMouseUp(e as JQuery.MouseUpEvent, x, y) const edge = edgeView.cell if (edge.hasLoop()) { this.makeLoopEdge(edge) } this.halo.stopBatch() this.halo.trigger('action:edge:addde', { edge }) this.edgeView = null } this.graph.view.delegateEvents() } makeLoopEdge(edge: Edge) { let vertex1: Point | null = null let vertex2: Point | null = null const loopEdgeWidth = this.options.loopEdgeWidth! const graphOptions = this.graph.options const graphRect = new Rectangle( 0, 0, graphOptions.width, graphOptions.height, ) const bbox = this.graph.graphToLocal(this.view.getBBox()) const found = [ this.options.loopEdgePreferredSide, 'top', 'bottom', 'left', 'right', ].some((position) => { let point: Point | null = null let dx = 0 let dy = 0 switch (position) { case 'top': point = new Point(bbox.x + bbox.width / 2, bbox.y - loopEdgeWidth) dx = loopEdgeWidth / 2 break case 'bottom': point = new Point( bbox.x + bbox.width / 2, bbox.y + bbox.height + loopEdgeWidth, ) dx = loopEdgeWidth / 2 break case 'left': point = new Point(bbox.x - loopEdgeWidth, bbox.y + bbox.height / 2) dy = loopEdgeWidth / 2 break case 'right': point = new Point( bbox.x + bbox.width + loopEdgeWidth, bbox.y + bbox.height / 2, ) dy = loopEdgeWidth / 2 break default: break } if (point) { vertex1 = point.translate(-dx, -dy) vertex2 = point.translate(dx, dy) return ( graphRect.containsPoint(vertex1) && graphRect.containsPoint(vertex2) ) } return false }) if (found && vertex1 && vertex2) { edge.setVertices([vertex1, vertex2]) } } // #endregion // #region resize startResize({ e }: Handle.EventArgs) { this.halo.startBatch() this.flip = [1, 0, 0, 1, 1, 0, 0, 1][ Math.floor(Angle.normalize(this.node.getAngle()) / 45) ] this.view.addClass('node-resizing') notify('node:resize', e as JQuery.MouseDownEvent, this.view as NodeView) } doResize({ e, dx, dy }: Handle.EventArgs) { const size = this.node.getSize() const width = Math.max(size.width + (this.flip ? dx : dy), 1) const height = Math.max(size.height + (this.flip ? dy : dx), 1) this.node.resize(width, height, { absolute: true, }) notify('node:resizing', e as JQuery.MouseMoveEvent, this.view as NodeView) } stopResize({ e }: Handle.EventArgs) { this.view.removeClass('node-resizing') notify('node:resized', e as JQuery.MouseUpEvent, this.view as NodeView) this.halo.stopBatch() } // #endregion // #region clone startClone({ e, x, y }: Handle.EventArgs) { this.halo.startBatch() const options = this.options const cloned = options.clone!(this.cell, { clone: true, }) if (!Cell.isCell(cloned)) { throw new Error("option 'clone()' has to return a cell") } this.centerNodeAtCursor(cloned, x, y) this.model.addCell(cloned, { halo: this.halo.cid, async: false, }) const cloneView = this.graph.renderer.findViewByCell(cloned) as NodeView cloneView.onMouseDown(e as JQuery.MouseDownEvent, x, y) this.halo.setEventData(e, { cloneView }) } centerNodeAtCursor(cell: Cell, x: number, y: number) { const center = cell.getBBox().getCenter() const dx = x - center.x const dy = y - center.y cell.translate(dx, dy) } doClone({ e, x, y }: Handle.EventArgs) { const view = this.halo.getEventData(e).cloneView as CellView if (view) { view.onMouseMove(e as JQuery.MouseMoveEvent, x, y) } } stopClone({ e, x, y }: Handle.EventArgs) { const nodeView = this.halo.getEventData(e).cloneView as NodeView if (nodeView) { nodeView.onMouseUp(e as JQuery.MouseUpEvent, x, y) } this.halo.stopBatch() } // #endregion // #region fork startFork({ e, x, y }: Handle.EventArgs) { this.halo.startBatch() const cloned = this.options.clone!(this.cell, { fork: true, }) if (!Cell.isCell(cloned)) { throw new Error("option 'clone()' has to return a cell") } this.centerNodeAtCursor(cloned, x, y) this.model.addCell(cloned, { halo: this.halo.cid, async: false, }) const edge = this.createEdgeConnectedToSource() const cloneView = this.graph.renderer.findViewByCell(cloned) as CellView const magnet = this.getMagnet(cloneView, 'target') const terminal = this.getEdgeTerminal(cloneView, magnet) edge.setTarget(terminal) this.model.addEdge(edge, { halo: this.halo.cid, async: false, }) cloneView.onMouseDown(e as JQuery.MouseDownEvent, x, y) this.halo.setEventData(e, { cloneView }) } doFork({ e, x, y }: Handle.EventArgs) { const view = this.halo.getEventData(e).cloneView as CellView if (view) { view.onMouseMove(e as JQuery.MouseMoveEvent, x, y) } } stopFork({ e, x, y }: Handle.EventArgs) { const view = this.halo.getEventData(e).cloneView as CellView if (view) { view.onMouseUp(e as JQuery.MouseUpEvent, x, y) } this.halo.stopBatch() } // #endregion // #region rotate startRotate({ e, x, y }: Handle.EventArgs) { this.halo.startBatch() const center = this.node.getBBox().getCenter() const nodes = [this.node] if (this.options.rotateEmbeds) { this.node .getDescendants({ deep: true, }) .reduce((memo, cell) => { if (cell.isNode()) { memo.push(cell) } return memo }, nodes) } this.halo.setEventData(e, { center, nodes, rotateStartAngles: nodes.map((node) => node.getAngle()), clientStartAngle: new Point(x, y).theta(center), }) nodes.forEach((node) => { const view = this.graph.findViewByCell(node) as NodeView if (view) { view.addClass('node-rotating') notify('node:rotate', e as JQuery.MouseDownEvent, view) } }) } doRotate({ e, x, y }: Handle.EventArgs) { const data = this.halo.getEventData(e) const delta = data.clientStartAngle - new Point(x, y).theta(data.center) data.nodes.forEach((node: Node, index: number) => { const startAngle = data.rotateStartAngles[index] const targetAngle = Util.snapToGrid( startAngle + delta, this.options.rotateGrid!, ) node.rotate(targetAngle, { absolute: true, center: data.center, halo: this.halo.cid, }) notify( 'node:rotating', e as JQuery.MouseMoveEvent, this.graph.findViewByCell(node) as NodeView, ) }) } stopRotate({ e }: Handle.EventArgs) { const data = this.halo.getEventData(e) data.nodes.forEach((node: Node) => { const view = this.graph.findViewByCell(node) as NodeView view.removeClass('node-rotating') notify('node:rotated', e as JQuery.MouseUpEvent, view) }) this.halo.stopBatch() } // #endregion // #region unlink unlink() { this.halo.startBatch() this.model.removeConnectedEdges(this.cell) this.halo.stopBatch() } // #endregion }