UNPKG

@logicflow/core

Version:

LogicFlow, help you quickly create flowcharts

1,061 lines (1,001 loc) 28 kB
import { pick, forEach } from 'lodash-es' import { getNodeBBox, isInNode, distance, sampleCubic } from '.' import LogicFlow from '../LogicFlow' import { Options } from '../options' import { SegmentDirection } from '../constant' import { getVerticalPointOfLine, getCrossPointOfLine, isInSegment, } from '../algorithm' import { Model, BaseNodeModel, BaseEdgeModel, LineEdgeModel, PolylineEdgeModel, GraphModel, } from '../model' import Point = LogicFlow.Point import Direction = LogicFlow.Direction import LineSegment = LogicFlow.LineSegment import NodeData = LogicFlow.NodeData import EdgeConfig = LogicFlow.EdgeConfig import Position = LogicFlow.Position import BoxBounds = Model.BoxBounds type PolyPointMap = Record<string, Point> type PolyPointLink = Record<string, string> /* 手动创建边时 edge -> edgeModel */ export const setupEdgeModel = (edge: EdgeConfig, graphModel: GraphModel) => { let model: BaseEdgeModel switch (edge.type) { case 'line': model = new LineEdgeModel(edge, graphModel) break case 'polyline': model = new PolylineEdgeModel(edge, graphModel) break default: model = new LineEdgeModel(edge, graphModel) break } return model } /* 判断两个Bbox是否重合 */ export const isBboxOverLapping = (b1: BoxBounds, b2: BoxBounds): boolean => Math.abs(b1.centerX - b2.centerX) * 2 < b1.width + b2.width && Math.abs(b1.centerY - b2.centerY) * 2 < b1.height + b2.height /* 连接点去重,去掉x,y位置重复的点 */ export const filterRepeatPoints = (points: Point[]): Point[] => { const result: Point[] = [] const pointsMap: Record<string, Point> = {} points.forEach((p) => { const id = `${p.x}-${p.y}` p.id = id pointsMap[id] = p }) Object.keys(pointsMap).forEach((p) => { result.push(pointsMap[p]) }) return result } /* 获取简单边:边之间除了起始点,只有1个中间点 */ export const getSimplePolyline = (sPoint: Point, tPoint: Point): Point[] => { const points = [ sPoint, { x: sPoint.x, y: tPoint.y, }, tPoint, ] return filterRepeatPoints(points) } /* 扩展的bbox,保证起始点的下一个点一定在node的垂直方向,不会与线重合, offset是点与线的垂直距离 */ export const getExpandedBBox = (bbox: BoxBounds, offset: number): BoxBounds => { if (bbox.width === 0 && bbox.height === 0) { return bbox } return { x: bbox.x, y: bbox.y, centerX: bbox.centerX, centerY: bbox.centerY, minX: bbox.minX - offset, minY: bbox.minY - offset, maxX: bbox.maxX + offset, maxY: bbox.maxY + offset, height: bbox.height + 2 * offset, width: bbox.width + 2 * offset, } } /* 判断点与中心点边的方向:是否水平,true水平,false垂直 */ export const pointDirection = (point: Point, bbox: BoxBounds): Direction => { const dx = Math.abs(point.x - bbox.centerX) const dy = Math.abs(point.y - bbox.centerY) return dx / bbox.width > dy / bbox.height ? SegmentDirection.HORIZONTAL : SegmentDirection.VERTICAL } /* 获取扩展图形上的点,即起始终点相邻的点,上一个或者下一个节点 */ export const getExpandedBBoxPoint = ( expendBBox: BoxBounds, bbox: BoxBounds, point: Point, ): Point => { // https://github.com/didi/LogicFlow/issues/817 // 没有修复前传入的参数bbox实际是expendBBox // 由于pointDirection使用的是dx / bbox.width > dy / bbox.height判定方向 // 此时的bbox.width是增加了offset后的宽度,bbox.height同理 // 这导致了部分极端情况(宽度很大或者高度很小),计算出来的方向错误 const direction = pointDirection(point, bbox) if (direction === SegmentDirection.HORIZONTAL) { return { x: point.x > expendBBox.centerX ? expendBBox.maxX : expendBBox.minX, y: point.y, } } return { x: point.x, y: point.y > expendBBox.centerY ? expendBBox.maxY : expendBBox.minY, } } /* 获取包含2个图形的外层box */ export const mergeBBox = (b1: BoxBounds, b2: BoxBounds): BoxBounds => { const minX = Math.min(b1.minX, b2.minX) const minY = Math.min(b1.minY, b2.minY) const maxX = Math.max(b1.maxX, b2.maxX) const maxY = Math.max(b1.maxY, b2.maxY) return { x: (minX + maxX) / 2, y: (minY + maxY) / 2, centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2, minX, minY, maxX, maxY, height: maxY - minY, width: maxX - minX, } } /* 获取多个点的外层bbox * 这个函数的用处 * 1、获取起始终点相邻点(expendBboxPoint)的bbox * 2、polylineEdge, bezierEdge,获取outline边框,这种情况传入offset */ export const getBBoxOfPoints = ( points: Point[] = [], offset?: number, ): BoxBounds => { const xList: number[] = [] const yList: number[] = [] points.forEach((p) => { xList.push(p.x) yList.push(p.y) }) const minX = Math.min(...xList) const maxX = Math.max(...xList) const minY = Math.min(...yList) const maxY = Math.max(...yList) let width = maxX - minX let height = maxY - minY if (offset) { width += offset height += offset } return { centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2, maxX, maxY, minX, minY, x: (minX + maxX) / 2, y: (minY + maxY) / 2, height, width, } } /* 获取box四个角上的点 */ export const getPointsFromBBox = ( bbox: BoxBounds, ): [Point, Point, Point, Point] => { const { minX, minY, maxX, maxY } = bbox return [ { x: minX, y: minY, }, { x: maxX, y: minY, }, { x: maxX, y: maxY, }, { x: minX, y: maxY, }, ] } /* 判断某一点是否在box中 */ export const isPointOutsideBBox = (point: Point, bbox: BoxBounds): boolean => { const { x, y } = point return x < bbox.minX || x > bbox.maxX || y < bbox.minY || y > bbox.maxY } /* 获取点的x方向上与box的交点 */ export const getBBoxXCrossPoints = ( bbox: BoxBounds, x: number, ): [Point, Point] | [] => { if (x < bbox.minX || x > bbox.maxX) { return [] } return [ { x, y: bbox.minY, }, { x, y: bbox.maxY, }, ] } /* 获取点的y方向上与box的交点 */ export const getBBoxYCrossPoints = ( bbox: BoxBounds, y: number, ): [Point, Point] | [] => { if (y < bbox.minY || y > bbox.maxY) { return [] } return [ { x: bbox.minX, y, }, { x: bbox.maxX, y, }, ] } /* 获取点的x,y方向上与box的交点 */ export const getBBoxCrossPointsByPoint = ( bbox: BoxBounds, point: Point, ): [Point, Point, Point, Point] | [Point, Point] | [] => [ ...getBBoxXCrossPoints(bbox, point.x), ...getBBoxYCrossPoints(bbox, point.y), ] /* 计算两点之间的预测距离(非直线距离) */ export const estimateDistance = (p1: Point, p2: Point): number => Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) /* 减少点别重复计算进距离的误差 */ export const costByPoints = (p: Point, points: Point[]): number => { const offset = -2 let result = 0 points.forEach((point) => { if (point) { if (p.x === point.x) { result += offset } if (p.y === point.y) { result += offset } } }) return result } /* 预估距离 */ export const heuristicCostEstimate = ( p: Point, ps: Point, pt: Point, source?: Point, target?: Point, ): number => estimateDistance(p, ps) + estimateDistance(p, pt) + costByPoints(p, [ps, pt, source!, target!]) /* 重建路径,根据cameFrom属性计算出从起始到结束的路径 */ export const rebuildPath = ( pathPoints: Point[], pointById: PolyPointMap, cameFrom: PolyPointLink, currentId: string, iterator?: number, ): void => { if (!iterator) { iterator = 0 } pathPoints.unshift(pointById[currentId]) if ( cameFrom[currentId] && cameFrom[currentId] !== currentId && iterator <= 100 ) { rebuildPath( pathPoints, pointById, cameFrom, cameFrom[currentId], iterator + 1, ) } } /* 把计算完毕的点从开放列表中删除 */ export const removeClosePointFromOpenList = ( arr: Point[], item: Point, ): void => { const index = arr.indexOf(item) if (index > -1) { arr.splice(index, 1) } } /* 通过向量判断线段之间是否是相交的 */ export const isSegmentsIntersected = ( p0: Point, p1: Point, p2: Point, p3: Point, ): boolean => { const s1x = p1.x - p0.x const s1y = p1.y - p0.y const s2x = p3.x - p2.x const s2y = p3.y - p2.y const s = (-s1y * (p0.x - p2.x) + s1x * (p0.y - p2.y)) / (-s2x * s1y + s1x * s2y) const t = (s2x * (p0.y - p2.y) - s2y * (p0.x - p2.x)) / (-s2x * s1y + s1x * s2y) return s >= 0 && s <= 1 && t >= 0 && t <= 1 } /* 判断线段与bbox是否是相交的,保证节点之间的边不会穿过节点自身 */ export const isSegmentCrossingBBox = ( p1: Point, p2: Point, bbox: BoxBounds, ): boolean => { if (bbox.width === 0 && bbox.height === 0) { return false } const [pa, pb, pc, pd] = getPointsFromBBox(bbox) return ( isSegmentsIntersected(p1, p2, pa, pb) || isSegmentsIntersected(p1, p2, pa, pd) || isSegmentsIntersected(p1, p2, pb, pc) || isSegmentsIntersected(p1, p2, pc, pd) ) } /* 获取下一个相邻的点 */ export const getNextNeighborPoints = ( points: Point[], point: Point, bbox1: BoxBounds, bbox2: BoxBounds, ): Point[] => { const neighbors: Point[] = [] points.forEach((p) => { if (p !== point) { if (p.x === point.x || p.y === point.y) { if ( !isSegmentCrossingBBox(p, point, bbox1) && !isSegmentCrossingBBox(p, point, bbox2) ) { neighbors.push(p) } } } }) return filterRepeatPoints(neighbors) } /* 路径查找,AStar查找+曼哈顿距离 * 算法wiki:https://zh.wikipedia.org/wiki/A*%E6%90%9C%E5%B0%8B%E6%BC%94%E7%AE%97%E6%B3%95 * 方法无法复用,且调用了很多polyline相关的方法,暂不抽离到src/algorithm中 */ export const pathFinder = ( points: Point[], start: Point, goal: Point, sBBox: BoxBounds, tBBox: BoxBounds, os: Point, ot: Point, ): Point[] => { // 定义已经遍历过的点 const closedSet: Point[] = [] // 定义需要遍历的店 const openSet = [start] // 定义节点的上一个节点 const cameFrom: PolyPointLink = {} const gScore: { [key: string]: number } = {} const fScore: { [key: string]: number } = {} if (start.id) { gScore[start.id] = 0 fScore[start.id] = heuristicCostEstimate(start, goal, start) } const pointById: PolyPointMap = {} points.forEach((p) => { if (p.id) { pointById[p.id] = p } }) while (openSet.length) { let current: Point | undefined let lowestFScore = Infinity openSet.forEach((p: Point) => { if (p.id && fScore[p.id] < lowestFScore) { lowestFScore = fScore[p.id] current = p } }) if (current === goal && goal.id) { const pathPoints: Point[] = [] rebuildPath(pathPoints, pointById, cameFrom, goal.id) return pathPoints } if (!current) { return [start, goal] } removeClosePointFromOpenList(openSet, current) closedSet.push(current) getNextNeighborPoints(points, current, sBBox, tBBox).forEach((neighbor) => { if (closedSet.indexOf(neighbor) !== -1) { return } if (openSet.indexOf(neighbor) === -1) { openSet.push(neighbor) } if (current?.id && neighbor?.id) { const tentativeGScore = fScore[current.id] + estimateDistance(current, neighbor) if (gScore[neighbor.id] && tentativeGScore >= gScore[neighbor.id]) { return } cameFrom[neighbor.id] = current.id gScore[neighbor.id] = tentativeGScore fScore[neighbor.id] = gScore[neighbor.id] + heuristicCostEstimate(neighbor, goal, start, os, ot) } }) } return [start, goal] } export const getBoxByOriginNode = (node: BaseNodeModel): BoxBounds => { return getNodeBBox(node) } /* 保证一条直线上只有2个节点: 删除x/y相同的中间节点 */ export const pointFilter = (points: Point[]): Point[] => { let i = 1 while (i < points.length - 1) { const pre = points[i - 1] const current = points[i] const next = points[i + 1] if ( (pre.x === current.x && current.x === next.x) || (pre.y === current.y && current.y === next.y) ) { points.splice(i, 1) } else { i++ } } return points } /* 计算折线点 */ export const getPolylinePoints = ( start: Point, end: Point, sNode: BaseNodeModel, tNode: BaseNodeModel, offset: number, ): Point[] => { const sBBox = getBoxByOriginNode(sNode) const tBBox = getBoxByOriginNode(tNode) const sxBBox = getExpandedBBox(sBBox, offset) const txBBox = getExpandedBBox(tBBox, offset) const sPoint = getExpandedBBoxPoint(sxBBox, sBBox, start) const tPoint = getExpandedBBoxPoint(txBBox, tBBox, end) // 当加上offset后的bbox有重合,直接简单计算节点 if (isBboxOverLapping(sxBBox, txBBox)) { const points = getSimplePoints(start, end, sPoint, tPoint) return [start, sPoint, ...points, tPoint, end] } const lineBBox = getBBoxOfPoints([sPoint, tPoint]) const sMixBBox = mergeBBox(sxBBox, lineBBox) const tMixBBox = mergeBBox(txBBox, lineBBox) let connectPoints: Point[] = [] connectPoints = connectPoints.concat(getPointsFromBBox(sMixBBox)) connectPoints = connectPoints.concat(getPointsFromBBox(tMixBBox)) // 中心点 const centerPoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2, } // 获取中心点与其他box的交点 ;[lineBBox, sMixBBox, tMixBBox].forEach((bbox: BoxBounds) => { connectPoints = connectPoints.concat( getBBoxCrossPointsByPoint(bbox, centerPoint).filter( (p) => isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox), ), ) }) // 与起止终点相邻的两的,在x,y方向上的交点,这四个点组成了矩形 。。。解释图中在不中这两个点, ;[ { x: sPoint.x, y: tPoint.y, }, { x: tPoint.x, y: sPoint.y, }, ].forEach((p) => { if (isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox)) { connectPoints.push(p) } }) connectPoints.unshift(sPoint) connectPoints.push(tPoint) connectPoints = filterRepeatPoints(connectPoints) // 路径查找-最关键的步骤 let pathPoints = pathFinder( connectPoints, sPoint, tPoint, sBBox, tBBox, start, end, ) pathPoints.unshift(start) pathPoints.push(end) // 删除一条直线上的多余节点 if (pathPoints.length > 2) { pathPoints = pointFilter(pathPoints) } return filterRepeatPoints(pathPoints) } /** * 获取折线中最长的一个线 * @param pointsList 多个点组成的数组 */ export const getLongestEdge = (pointsList: Point[]): [Point, Point] => { if (pointsList.length === 1) { const [point] = pointsList return [point, point] } else { let point1 = pointsList[0] let point2 = pointsList[1] let edgeLength = distance(point1.x, point1.y, point2.x, point2.y) for (let i = 1; i < pointsList.length - 1; i++) { const newPoint1 = pointsList[i] const newPoint2 = pointsList[i + 1] const newEdgeLength = distance( newPoint1.x, newPoint1.y, newPoint2.x, newPoint2.y, ) if (newEdgeLength > edgeLength) { edgeLength = newEdgeLength point1 = newPoint1 point2 = newPoint2 } } return [point1, point2] } } /* 线段是否在节点内部,被包含了 */ export const isSegmentsInNode = ( start: Point, end: Point, node: BaseNodeModel, ): boolean => { const startInNode = isInNode(start, node) const endInNode = isInNode(end, node) return startInNode && endInNode } /* 线段是否与节点相交 */ export const isSegmentsCrossNode = ( start: Point, end: Point, node: BaseNodeModel, ): boolean => { const startInNode = isInNode(start, node) const endInNode = isInNode(end, node) // bothInNode,线段两个端点都在节点内 const bothInNode = startInNode && endInNode // cross,线段有端点在节点内 const inNode = startInNode || endInNode // 有且只有一个点在节点内部 return !bothInNode && inNode } /* 获取线段在矩形内部的交点 */ export const getCrossPointInRect = ( start: Point, end: Point, node: BaseNodeModel, ): Point | false | undefined => { let crossSegments: [Point, Point] | undefined = undefined const nodeBox = getNodeBBox(node) const points = getPointsFromBBox(nodeBox) for (let i = 0; i < points.length; i++) { const isCross = isSegmentsIntersected( start, end, points[i], points[(i + 1) % points.length], ) if (isCross) { crossSegments = [points[i], points[(i + 1) % points.length]] } } if (crossSegments) { return getCrossPointOfLine(start, end, crossSegments[0], crossSegments[1]) } } /* 判断线段的方向 */ export const segmentDirection = ( start: Point, end: Point, ): Direction | undefined => { let direction: Direction | undefined = undefined if (start.x === end.x) { direction = SegmentDirection.VERTICAL } else if (start.y === end.y) { direction = SegmentDirection.HORIZONTAL } return direction } export const points2PointsList = (points: string): Point[] => { const currentPositionList = points.split(' ') const pointsList: LogicFlow.Position[] = [] currentPositionList && currentPositionList.forEach((item) => { const [x, y] = item.split(',') pointsList.push({ x: Number(x), y: Number(y), }) }) return pointsList } export const getSimplePoints = ( start: Point, end: Point, sPoint: Point, tPoint: Point, ): Point[] => { const points: LogicFlow.Position[] = [] // start,sPoint的方向,水平或者垂直,即路径第一条线段的方向 const startDirection = segmentDirection(start, sPoint)! // end,tPoint的方向,水平或者垂直,即路径最后一条条线段的方向 const endDirection = segmentDirection(end, tPoint)! // 根据两条线段的方向作了计算,调整线段经验所得,非严格最优计算,能保证不出现折线 // 方向相同,添加两个点,两条平行线垂直距离一半的两个端点 if (startDirection === endDirection) { if (start.y === sPoint.y) { points.push({ x: sPoint.x, y: (sPoint.y + tPoint.y) / 2, }) points.push({ x: tPoint.x, y: (sPoint.y + tPoint.y) / 2, }) } else { points.push({ x: (sPoint.x + tPoint.x) / 2, y: sPoint.y, }) points.push({ x: (sPoint.x + tPoint.x) / 2, y: tPoint.y, }) } } else { // 方向不同,添加一个点,保证不在当前线段上(会出现重合),且不能有折线 let point = { x: sPoint.x, y: tPoint.y, } const inStart = isInSegment(point, start, sPoint) const inEnd = isInSegment(point, end, tPoint) if (inStart || inEnd) { point = { x: tPoint.x, y: sPoint.y, } } else { const onStart = isOnLine(point, start, sPoint) const onEnd = isOnLine(point, end, tPoint) if (onStart && onEnd) { point = { x: tPoint.x, y: sPoint.y, } } } points.push(point) } return points } const isOnLine = (point: Point, start: Point, end: Point) => (point.x === start.x && point.x === end.x) || (point.y === start.y && point.y === end.y) /* 求字符串的字节长度 */ export const getBytesLength = (word: string): number => { if (!word) { return 0 } let totalLength = 0 for (let i = 0; i < word.length; i++) { const c = word.charCodeAt(i) if (word.match(/[A-Z]/)) { totalLength += 1.5 } else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) { totalLength += 1 } else { totalLength += 2 } } return totalLength } /** * Uses canvas.measureText to compute * and return the width of the given text of given font in pixels. * @param {String} text The text to be rendered. * @param {String} font The css font descriptor * that text is to be rendered with (e.g. "bold 14px verdana"). * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 */ let canvas: HTMLCanvasElement | undefined = undefined export const getTextWidth = (text: string, font: string) => { if (!canvas) { canvas = document.createElement('canvas') } const context = canvas.getContext('2d') context!.font = font const metrics = context!.measureText(text) return metrics.width } type AppendAttributesType = { d: string fill: string stroke: string strokeWidth: number strokeDasharray: string } // 扩大边可点区域,获取边append的信息 export const getAppendAttributes = ( appendInfo: Record<'start' | 'end', Point>, ): AppendAttributesType => { const { start, end } = appendInfo let d: string if (start.x === end.x && start.y === end.y) { // 拖拽过程中会出现起终点重合的情况,这时候append无法计算 d = '' } else { const config = { start, end, offset: 10, verticalLength: 5, } const startPosition = getVerticalPointOfLine({ ...config, type: 'start', }) const endPosition = getVerticalPointOfLine({ ...config, type: 'end', }) d = `M${startPosition.leftX} ${startPosition.leftY} L${startPosition.rightX} ${startPosition.rightY} L${endPosition.rightX} ${endPosition.rightY} L${endPosition.leftX} ${endPosition.leftY} z` } return { d, fill: 'transparent', stroke: 'transparent', strokeWidth: 1, strokeDasharray: '4, 4', } } export type IBezierControls = { sNext: Point ePre: Point } // bezier曲线 export const getBezierControlPoints = ({ start, end, sourceNode, targetNode, offset, }: { start: Point end: Point sourceNode: BaseNodeModel targetNode: BaseNodeModel offset: number }): IBezierControls => { const sBBox = getNodeBBox(sourceNode) const tBBox = getNodeBBox(targetNode) const sExpendBBox = getExpandedBBox(sBBox, offset) const tExpendBBox = getExpandedBBox(tBBox, offset) const sNext = getExpandedBBoxPoint(sExpendBBox, sBBox, start) const ePre = getExpandedBBoxPoint(tExpendBBox, tBBox, end) return { sNext, ePre, } } export type IBezierPoints = { start: Point sNext: Point ePre: Point end: Point } // 根据bezier曲线path求出Points export const getBezierPoints = (path: string): [Point, Point, Point, Point] => { const list = path.replace(/M/g, '').replace(/C/g, ',').split(',') const start = getBezierPoint(list[0]) const sNext = getBezierPoint(list[1]) const ePre = getBezierPoint(list[2]) const end = getBezierPoint(list[3]) return [start, sNext, ePre, end] } // 根据bezier曲线path求出Point坐标 const getBezierPoint = (positionStr: string): Point => { const [x, y] = positionStr.replace(/(^\s*)/g, '').split(' ') return { x: +x, y: +y, } } // 根据bezier曲线path求出结束切线的两点坐标 export const getEndTangent = ( pointsList: Point[], offset: number, ): [Point, Point] => { // const bezierPoints = getBezierPoints(path) const [p1, cp1, cp2, p2] = pointsList const start = sampleCubic(p1, cp1, cp2, p2, offset) return [start, pointsList[3]] } /** * 获取移动边后,文本位置距离边上的最近的一点 * @param point 边上文本的位置 * @param points 边的各个拐点 * TODO: Label实验没问题后统一改成新的计算方式,把这个方法废弃 */ export const getClosestPointOfPolyline = ( point: Point, points: string, ): Point => { const { x, y } = point const pointsPosition = points2PointsList(points) let minDistance = Number.MAX_SAFE_INTEGER let crossPoint: Point const segments: LineSegment[] = [] for (let i = 0; i < pointsPosition.length; i++) { segments.push({ start: pointsPosition[i], end: pointsPosition[(i + 1) % pointsPosition.length], }) } segments.forEach((item) => { const { start, end } = item // 若线段垂直,则crossPoint的横坐标与线段一致 if (start.x === end.x) { const pointXY = { x: start.x, y, } const inSegment = isInSegment(pointXY, start, end) if (inSegment) { const currentDistance = Math.abs(start.x - x) if (currentDistance < minDistance) { minDistance = currentDistance crossPoint = pointXY } } } else if (start.y === end.y) { const pointXY = { x, y: start.y, } const inSegment = isInSegment(pointXY, start, end) if (inSegment) { const currentDistance = Math.abs(start.y - y) if (currentDistance < minDistance) { minDistance = currentDistance crossPoint = pointXY } } } }) // 边界:只有一条线段时,沿线段移动节点,当文本超出边后,文本没有可供参考的线段 if (!crossPoint!) { const { start, end } = segments[0] crossPoint = { x: start.x + (end.x - start.x) / 2, y: start.y + (end.y - start.y) / 2, } } return crossPoint } // 规范边初始化数据 export const pickEdgeConfig = (data: EdgeConfig): EdgeConfig => pick(data, [ 'id', 'type', 'sourceNodeId', 'sourceAnchorId', 'targetNodeId', 'targetAnchorId', 'pointsList', 'startPoint', 'endPoint', 'properties', ]) export const twoPointDistance = (source: Position, target: Position) => { // fix: 修复坐标存在负值时计算错误的问题。 // const source = { // x: p1.x, // y: Math.abs(p1.y), // } // const target = { // x: Math.abs(p2.x), // y: Math.abs(p2.y), // } return Math.sqrt((source.x - target.x) ** 2 + (source.y - target.y) ** 2) } /** * 包装边生成函数 * @param graphModel graph model * @param generator 用户自定义的边生成函数 */ export function createEdgeGenerator( graphModel: GraphModel, generator?: Options.EdgeGeneratorType | unknown, ): any { // TODO: 定义返回值类型,保证 GraphModel 中类型的正确性 if (typeof generator !== 'function') { return ( _sourceNode: NodeData, _targetNode: NodeData, currentEdge?: EdgeConfig, ) => Object.assign({ type: graphModel.edgeType }, currentEdge) } return ( sourceNode: NodeData, targetNode: NodeData, currentEdge?: EdgeConfig, ) => { const result = generator(sourceNode, targetNode, currentEdge) // 无结果使用默认类型 if (!result) return { type: graphModel.edgeType } if (typeof result === 'string') { return Object.assign({}, currentEdge, { type: result }) } return Object.assign({ type: result }, currentEdge) } } // 获取 Svg 标签文案高度,自动换行 export type IGetSvgTextSizeParams = { rows: string[] rowsLength: number fontSize: number } export const getSvgTextSize = ({ rows, rowsLength, fontSize, }: IGetSvgTextSizeParams): LogicFlow.RectSize => { let longestBytes = 0 forEach(rows, (row) => { const rowBytesLength = getBytesLength(row) longestBytes = rowBytesLength > longestBytes ? rowBytesLength : longestBytes }) // 背景框宽度,最长一行字节数/2 * fontsize + 2 // 背景框宽度, 行数 * fontsize + 2 return { width: Math.ceil(longestBytes / 2) * fontSize + fontSize / 4, height: rowsLength * (fontSize + 2) + fontSize / 4, } }