UNPKG

@logicflow/core

Version:

LogicFlow, help you quickly create flowcharts

673 lines (642 loc) 19.9 kB
import { get, assign, cloneDeep } from 'lodash-es' import { observable, action } from 'mobx' import { BaseEdgeModel } from '.' import { BaseNodeModel, RectNodeModel, CircleNodeModel, Model } from '..' import LogicFlow from '../../LogicFlow' import { ModelType, SegmentDirection } from '../../constant' import { isInNode, distance, getClosestRadiusCenter, inStraightLineOfRect, getCrossPointWithCircle, getCrossPointWithEllipse, getCrossPointWithPolygon, getPolylinePoints, getLongestEdge, getCrossPointInRect, isSegmentsInNode, isSegmentsCrossNode, segmentDirection, points2PointsList, pointFilter, } from '../../util' import Point = LogicFlow.Point import Position = LogicFlow.Position import AppendConfig = LogicFlow.AppendConfig import AnchorConfig = Model.AnchorConfig export class PolylineEdgeModel extends BaseEdgeModel { modelType = ModelType.POLYLINE_EDGE draggingPointList: Point[] = [] @observable offset?: number @observable dbClickPosition?: Point initEdgeData(data: LogicFlow.EdgeConfig): void { this.offset = get(data, 'properties.offset', 30) if (data.pointsList) { this.pointsList = data.pointsList } super.initEdgeData(data) } getEdgeStyle() { const { polyline } = this.graphModel.theme const style = super.getEdgeStyle() const { style: customStyle = {} } = this.properties return { ...style, ...cloneDeep(polyline), ...cloneDeep(customStyle), } } getTextPosition() { // 在文本为空的情况下,文本位置为双击位置 const textValue = this.text?.value if (this.dbClickPosition && !textValue) { const { x, y } = this.dbClickPosition return { x, y } } // 文本不为空或者没有双击位置时,取最长边的中点作为文本位置 const currentPositionList = points2PointsList(this.points) const [p1, p2] = getLongestEdge(currentPositionList) return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, } } // 获取下一个锚点 getAfterAnchor( direction: SegmentDirection, position: Position, anchorList: AnchorConfig[], ) { let anchor: AnchorConfig let minDistance: number anchorList.forEach((item) => { let distanceX: number if (direction === SegmentDirection.HORIZONTAL) { distanceX = Math.abs(position.y - item.y) } else if (direction === SegmentDirection.VERTICAL) { distanceX = Math.abs(position.x - item.x) } if (!minDistance || minDistance > distanceX!) { minDistance = distanceX! anchor = item } }) return anchor! } /* 获取拖拽过程中产生的交点 */ getCrossPoint(direction: SegmentDirection, start: Position, end: Position) { let position: Point if (direction === SegmentDirection.HORIZONTAL) { position = { x: end.x, y: start.y, } } else if (direction === SegmentDirection.VERTICAL) { position = { x: start.x, y: end.y, } } return position! } // 删除在图形内的过个交点 removeCrossPoints(startIndex: number, endIndex: number, pointList: Point[]) { const list = pointList.map((i) => i) if (startIndex === 1) { const start = list[startIndex] const end = list[endIndex] const pre = list[startIndex - 1] const isInStartNode = isSegmentsInNode(pre, start, this.sourceNode) if (isInStartNode) { const isSegmentsCrossStartNode = isSegmentsCrossNode( start, end, this.sourceNode, ) if (isSegmentsCrossStartNode) { const point = getCrossPointInRect(start, end, this.sourceNode) if (point) { list[startIndex] = point list.splice(startIndex - 1, 1) startIndex-- endIndex-- } } } else { const anchorList = this.sourceNode.anchors anchorList.forEach((item) => { if ( (item.x === pre.x && item.x === start.x) || (item.y === pre.y && item.y === start.y) ) { const distance1 = distance(item.x, item.y, start.x, start.y) const distance2 = distance(pre.x, pre.y, start.x, start.y) if (distance1 < distance2) { list[startIndex - 1] = item } } }) } } if (endIndex === pointList.length - 2) { const start = list[startIndex] const end = list[endIndex] const next = list[endIndex + 1] const isInEndNode = isSegmentsInNode(end, next, this.targetNode) if (isInEndNode) { const isSegmentsCrossStartNode = isSegmentsCrossNode( start, end, this.targetNode, ) if (isSegmentsCrossStartNode) { const point = getCrossPointInRect(start, end, this.targetNode) if (point) { list[endIndex] = point list.splice(endIndex + 1, 1) } } } else { const anchorList = this.targetNode.anchors anchorList.forEach((item) => { if ( (item.x === next.x && item.x === end.x) || (item.y === next.y && item.y === end.y) ) { const distance1 = distance(item.x, item.y, end.x, end.y) const distance2 = distance(next.x, next.y, end.x, end.y) if (distance1 < distance2) { list[endIndex + 1] = item } } }) } } return list } // 获取在拖拽过程中可能产生的点 getDraggingPoints( direction: SegmentDirection, positionType: string, position: Position, anchorList: AnchorConfig[], draggingPointList: Point[], ) { const pointList = draggingPointList.map((i) => i) const anchor = this.getAfterAnchor(direction, position, anchorList) const crossPoint = this.getCrossPoint(direction, position, anchor) if (positionType === 'start') { pointList.unshift(crossPoint) pointList.unshift(anchor) } else { pointList.push(crossPoint) pointList.push(anchor) } return pointList } // 更新相交点[起点,终点],更加贴近图形, 未修改observable不作为action updateCrossPoints(pointList: Point[]) { const list = pointList.map((i) => i) const start = pointList[0] const next = pointList[1] const pre = pointList[list.length - 2] const end = pointList[list.length - 1] const { sourceNode, targetNode } = this const sourceModelType = sourceNode.modelType const targetModelType = targetNode.modelType const startPointDirection = segmentDirection(start, next)! let startCrossPoint = list[0] switch (sourceModelType) { case ModelType.RECT_NODE: if ((sourceNode as RectNodeModel).radius !== 0) { const inInnerNode = inStraightLineOfRect(start, sourceNode) if (!inInnerNode) { startCrossPoint = getClosestRadiusCenter( start, startPointDirection, sourceNode, ) } } break case ModelType.CIRCLE_NODE: startCrossPoint = getCrossPointWithCircle( start, startPointDirection, sourceNode as CircleNodeModel, ) break case ModelType.ELLIPSE_NODE: startCrossPoint = getCrossPointWithEllipse( start, startPointDirection, sourceNode, ) break case ModelType.DIAMOND_NODE: startCrossPoint = getCrossPointWithPolygon( start, startPointDirection, sourceNode, ) break case ModelType.POLYGON_NODE: startCrossPoint = getCrossPointWithPolygon( start, startPointDirection, sourceNode, ) break default: break } // 如果线段和形状没有交点时startCrossPoint会为undefined导致后续计算报错 if (startCrossPoint) { list[0] = startCrossPoint } const endPointDirection = segmentDirection(pre, end)! let endCrossPoint = list[list.length - 1] switch (targetModelType) { case ModelType.RECT_NODE: if ((targetNode as RectNodeModel).radius !== 0) { const inInnerNode = inStraightLineOfRect(end, targetNode) if (!inInnerNode) { endCrossPoint = getClosestRadiusCenter( end, endPointDirection, targetNode, ) } } break case ModelType.CIRCLE_NODE: endCrossPoint = getCrossPointWithCircle( end, endPointDirection, targetNode as CircleNodeModel, ) break case ModelType.ELLIPSE_NODE: endCrossPoint = getCrossPointWithEllipse( end, endPointDirection, targetNode, ) break case ModelType.DIAMOND_NODE: endCrossPoint = getCrossPointWithPolygon( end, endPointDirection, targetNode, ) break case ModelType.POLYGON_NODE: endCrossPoint = getCrossPointWithPolygon( end, endPointDirection, targetNode, ) break default: break } // 如果线段和形状没有交点时startCrossPoint会为undefined导致后续计算报错 if (endCrossPoint) { list[list.length - 1] = endCrossPoint } return list } updatePath(pointList: Point[]) { this.pointsList = pointList this.points = this.getPath(this.pointsList) } getData() { const data = super.getData() const pointsList = this.pointsList.map(({ x, y }) => ({ x, y, })) return Object.assign({}, data, { pointsList, }) } getPath(points: Point[]): string { return points.map((point) => `${point.x},${point.y}`).join(' ') } @action initPoints() { if (this.pointsList.length > 0) { this.points = this.getPath(this.pointsList) } else { this.updatePoints() } } @action updatePoints() { const pointsList = getPolylinePoints( { x: this.startPoint.x, y: this.startPoint.y, }, { x: this.endPoint.x, y: this.endPoint.y, }, this.sourceNode, this.targetNode, this.offset || 0, ) this.pointsList = pointsList this.points = pointsList.map((point) => `${point.x},${point.y}`).join(' ') } @action updateStartPoint(anchor: Point) { this.startPoint = Object.assign({}, anchor) this.updatePoints() } @action moveStartPoint(deltaX: number, deltaY: number): void { this.startPoint.x += deltaX this.startPoint.y += deltaY this.updatePoints() // todo: 尽量保持边的整体轮廓, 通过deltaX和deltaY更新pointsList,而不是重新计算。 } @action updateEndPoint(anchor: Point) { this.endPoint = Object.assign({}, anchor) this.updatePoints() } @action moveEndPoint(deltaX: number, deltaY: number): void { this.endPoint.x += deltaX this.endPoint.y += deltaY this.updatePoints() } @action updatePointsList(deltaX: number, deltaY: number): void { this.pointsList.forEach((item) => { item.x += deltaX item.y += deltaY }) const startPoint = this.pointsList[0] this.startPoint = Object.assign({}, startPoint) const endPoint = this.pointsList[this.pointsList.length - 1] this.endPoint = Object.assign({}, endPoint) this.initPoints() } @action dragAppendStart() { // mobx observer 对象被iterator处理会有问题 this.draggingPointList = this.pointsList.map((i) => i) } @action dragAppendSimple( appendInfo: AppendConfig, dragInfo: Record<'x' | 'y', number>, ) { // 因为drag事件是mouseDown事件触发的,因此当真实拖拽之后再设置isDragging // 避免因为点击事件造成,在dragStart触发之后,没有触发dragEnd错误设置了isDragging状态,对history计算造成错误 this.isDragging = true const { start, end, startIndex, endIndex, direction } = appendInfo const { pointsList } = this let draggingPointList = pointsList if (direction === SegmentDirection.HORIZONTAL) { // 水平,仅调整y坐标,拿到当前线段两个端点移动后的坐标 pointsList[startIndex] = { x: start.x, y: start.y + dragInfo.y, } pointsList[endIndex] = { x: end.x, y: end.y + dragInfo.y, } draggingPointList = this.pointsList.map((i) => i) } else if (direction === SegmentDirection.VERTICAL) { // 垂直,仅调整x坐标, 与水平调整同理 pointsList[startIndex] = { x: start.x + dragInfo.x, y: start.y, } pointsList[endIndex] = { x: end.x + dragInfo.x, y: end.y, } draggingPointList = this.pointsList.map((i) => i) } this.updatePointsAfterDrag(draggingPointList) this.draggingPointList = draggingPointList // TODO: 判断该逻辑是否需要 if (this.text?.value) { this.setText(assign({}, this.text, this.textPosition)) } return { start: assign({}, pointsList[startIndex]), end: assign({}, pointsList[endIndex]), startIndex, endIndex, direction, } } @action dragAppend(appendInfo: AppendConfig, dragInfo: Record<'x' | 'y', number>) { this.isDragging = true const { start, end, startIndex, endIndex, direction } = appendInfo const { pointsList } = this if (direction === SegmentDirection.HORIZONTAL) { // 水平,仅调整y坐标 // step1: 拿到当前线段两个端点移动后的坐标 pointsList[startIndex] = { x: start.x, y: start.y + dragInfo.y, } pointsList[endIndex] = { x: end.x, y: end.y + dragInfo.y, } // step2: 计算拖拽后,两个端点与节点外框的交点 // 定义一个拖住中节点list let draggingPointList = this.pointsList.map((i) => i) if (startIndex !== 0 && endIndex !== this.pointsList.length - 1) { // 2.1)如果线段没有连接起终点,过滤会穿插在图形内部的线段,取整个图形离线段最近的点 draggingPointList = this.removeCrossPoints( startIndex, endIndex, draggingPointList, ) } if (startIndex === 0) { // 2.2)如果线段连接了起点, 判断起点是否在节点内部 const startPosition = { x: start.x, y: start.y + dragInfo.y, } const inNode = isInNode(startPosition, this.sourceNode) if (!inNode) { // 如果不在节点内部,更换起点为线段与节点的交点 const anchorList = this.sourceNode.anchors draggingPointList = this.getDraggingPoints( direction, 'start', startPosition, anchorList, draggingPointList, ) } } if (endIndex === this.pointsList.length - 1) { // 2.2)如果线段连接了终点, 判断起点是否在节点内部 const endPosition = { x: end.x, y: end.y + dragInfo.y, } const inNode = isInNode(endPosition, this.targetNode) if (!inNode) { // 如果不在节点内部,更换终点为线段与节点的交点 const anchorList = this.targetNode.anchors draggingPointList = this.getDraggingPoints( direction, 'end', endPosition, anchorList, draggingPointList, ) } } this.updatePointsAfterDrag(draggingPointList) // step3: 调整到对应外框的位置后,执行updatePointsAfterDrag,找到当前线段和图形的准确交点 this.draggingPointList = draggingPointList } else if (direction === SegmentDirection.VERTICAL) { // 垂直,仅调整x坐标, 与水平调整同理 pointsList[startIndex] = { x: start.x + dragInfo.x, y: start.y, } pointsList[endIndex] = { x: end.x + dragInfo.x, y: end.y, } let draggingPointList = this.pointsList.map((i) => i) if (startIndex !== 0 && endIndex !== this.pointsList.length - 1) { draggingPointList = this.removeCrossPoints( startIndex, endIndex, draggingPointList, ) } if (startIndex === 0) { const startPosition = { x: start.x + dragInfo.x, y: start.y, } const inNode = isInNode(startPosition, this.sourceNode) if (!inNode) { const anchorList = this.sourceNode.anchors draggingPointList = this.getDraggingPoints( direction, 'start', startPosition, anchorList, draggingPointList, ) } } if (endIndex === this.pointsList.length - 1) { const endPosition = { x: end.x + dragInfo.x, y: end.y, } const inNode = isInNode(endPosition, this.targetNode) if (!inNode) { const anchorList = this.targetNode.anchors draggingPointList = this.getDraggingPoints( direction, 'end', endPosition, anchorList, draggingPointList, ) } } this.updatePointsAfterDrag(draggingPointList) this.draggingPointList = draggingPointList } // TODO: 确认该判断逻辑是否需要 if (this.text?.value) { this.setText(assign({}, this.text, this.textPosition)) } return { start: assign({}, pointsList[startIndex]), end: assign({}, pointsList[endIndex]), startIndex, endIndex, direction, } } @action dragAppendEnd() { if (this.draggingPointList) { const pointsList = pointFilter(points2PointsList(this.points)) // 更新pointsList,重新渲染appendWidth this.pointsList = pointsList.map((i) => i) // draggingPointList清空 this.draggingPointList = [] // 更新起终点 const startPoint = pointsList[0] this.startPoint = assign({}, startPoint) const endPoint = pointsList[pointsList.length - 1] this.endPoint = assign({}, endPoint) } this.isDragging = false } /* 拖拽之后个更新points,仅更新边,不更新pointsList, appendWidth会依赖pointsList,更新pointsList会重新渲染appendWidth,从而导致不能继续拖拽 在拖拽结束后再进行pointsList的更新 */ @action updatePointsAfterDrag(pointsList: Point[]) { // 找到准确的连接点后,更新points, 更新边,同时更新依赖points的箭头 const list = this.updateCrossPoints(pointsList) this.points = list.map((point) => `${point.x},${point.y}`).join(' ') } // 获取边调整的起点 @action getAdjustStart() { return this.pointsList[0] || this.startPoint } // 获取边调整的终点 @action getAdjustEnd() { const { pointsList } = this return pointsList[pointsList.length - 1] || this.endPoint } // 起终点拖拽调整过程中,进行折线路径更新 @action updateAfterAdjustStartAndEnd({ startPoint, endPoint, sourceNode, targetNode, }: { startPoint: Point endPoint: Point sourceNode: BaseNodeModel targetNode: BaseNodeModel }) { this.pointsList = getPolylinePoints( { x: startPoint.x, y: startPoint.y, }, { x: endPoint.x, y: endPoint.y, }, sourceNode, targetNode, this.offset || 0, ) this.initPoints() } } export default PolylineEdgeModel