UNPKG

@logicflow/extension

Version:
692 lines (620 loc) 14.7 kB
import { BBoxType } from './flow' import LogicFlow from '@logicflow/core' import Point = LogicFlow.Point class NodeBase { x: any y: any G: number H: number isProcessed: boolean connection: any from: any constructor(x: number, y: number) { this.x = x this.y = y this.G = 0 this.H = 0 this.isProcessed = false this.connection = null this.from = null } get F() { return this.G + this.H } setProcessed() { this.isProcessed = true } setConnection(connection: any) { this.connection = connection } setFrom(from: any) { this.from = from } setG(g: number) { this.G = g } setH(h: number) { this.H = h } getManhattanDistanceTo(point: { x: number; y: number }) { const { x: x1, y: y1 } = this const { x: x2, y: y2 } = point return Math.abs(x1 - x2) + Math.abs(y1 - y2) } } export class PriorityQueue { heap: any[] constructor() { this.heap = [] } enqueue(node: { x: never; y: never }, priority: number) { this.heap.push({ node, priority, }) this.bubbleUp(this.heap.length - 1) } dequeue() { const min = this.heap[0] const end = this.heap.pop() if (this.heap.length > 0) { this.heap[0] = end this.sinkDown(0) } return min } bubbleUp(index: number) { const node = this.heap[index] while (index > 0) { const parentIndex = Math.floor((index - 1) / 2) const parent = this.heap[parentIndex] if (node.priority >= parent.priority) break this.heap[parentIndex] = node this.heap[index] = parent index = parentIndex } } sinkDown(index: number) { const leftChildIndex = 2 * index + 1 const rightChildIndex = 2 * index + 2 let smallestChildIndex = index const { length } = this.heap if ( leftChildIndex < length && this.heap[leftChildIndex].priority < this.heap[smallestChildIndex].priority ) { smallestChildIndex = leftChildIndex } if ( rightChildIndex < length && this.heap[rightChildIndex].priority < this.heap[smallestChildIndex].priority ) { smallestChildIndex = rightChildIndex } if (smallestChildIndex !== index) { const swapNode = this.heap[smallestChildIndex] this.heap[smallestChildIndex] = this.heap[index] this.heap[index] = swapNode this.sinkDown(smallestChildIndex) } } isEmpty() { return this.heap.length === 0 } } function expandBBox(bbox: BBoxType, offset: number) { const { minX, minY, maxX, maxY } = bbox return { minX: minX - offset, minY: minY - offset, maxX: maxX + offset, maxY: maxY + offset, } } function getPointsFromBBoxBorder(bbox: BBoxType) { const { minX, minY, maxX, maxY } = bbox return [ { x: minX, y: minY, }, { x: minX + (maxX - minX) / 2, y: minY, }, { x: maxX, y: minY, }, { x: maxX, y: minY + (maxY - minY) / 2, }, { x: maxX, y: maxY, }, { x: minX + (maxX - minX) / 2, y: maxY, }, { x: minX, y: maxY, }, { x: minX, y: minY + (maxY - minY) / 2, }, ] } function getHull(points: any[]) { const xs = points.map((item: { x: any }) => item.x) const ys = points.map((item: { y: any }) => item.y) return { minX: Math.min(...xs), minY: Math.min(...ys), maxX: Math.max(...xs), maxY: Math.max(...ys), } } function isPointInsideTheBoxes(point: NodeBase | Point, bboxes: BBoxType[]) { let flag = false for (const bbox of bboxes) { if (isBBoxContainThePoint(bbox, point)) { flag = true break } } return flag } function isBBoxContainThePoint(bbox: BBoxType, p: NodeBase | Point) { const { x, y } = p const { minX, minY, maxX, maxY } = bbox // ignore the point on the border return x > minX && x < maxX && y > minY && y < maxY } function isSegmentsIntersected(seg1: any[], seg2: any[]) { const [p0, p1] = seg1 const [p2, p3] = seg2 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 } function getVerticesFromBBox(bbox: BBoxType) { const { minX, minY, maxX, maxY } = bbox return [ { x: minX, y: minY, }, { x: maxX, y: minY, }, { x: maxX, y: maxY, }, { x: minX, y: maxY, }, ] } function isSegmentCrossingBBox(line: any, bbox: BBoxType) { const [p1, p2] = line const { minX, minY, maxX, maxY } = bbox const width = Math.abs(maxX - minX) const height = Math.abs(maxY - minY) if (width === 0 && height === 0) { return false } const [pa, pb, pc, pd] = getVerticesFromBBox(bbox) let count = 0 if (isSegmentsIntersected([p1, p2], [pa, pb])) { count++ } if (isSegmentsIntersected([p1, p2], [pa, pd])) { count++ } if (isSegmentsIntersected([p1, p2], [pb, pc])) { count++ } if (isSegmentsIntersected([p1, p2], [pc, pd])) { count++ } return count !== 0 } function aStarFindPathByGrid( startNode: NodeBase, endNode: NodeBase, step: number, bboxes: any[], outside: BBoxType, ) { let toSearch = [startNode] const searchSet = new Set() while (toSearch.length) { let current = toSearch[0] for (const item of toSearch) { if (item.F < current.F || (item.F === current.F && item.H < current.H)) { current = item } } if (`${current.x}/${current.y}` === `${endNode.x}/${endNode.y}`) { const res = [ { x: current.x, y: current.y, }, ] while (current.connection) { const { connection } = current res.push({ x: connection.x, y: connection.y, }) current = current.connection } return res.reverse() } const val = `${current.x}/${current.y}` !searchSet.has(val) && searchSet.add(val) toSearch = toSearch.filter( (item) => `${current.x}/${current.y}` !== `${item.x}/${item.y}`, ) const neighborsRes = findNeighborsByGridStep( current, step, bboxes, outside, ).filter((item) => { const flag = !isPointInsideTheBoxes(item, bboxes) return flag }) const tmpRes: NodeBase[] = [] neighborsRes.forEach((item: NodeBase) => { const key = `${item.x}/${item.y}` if (!searchSet.has(key)) { tmpRes.push(item) tmpRes.push(item) } }) for (const neighbor of tmpRes) { if (neighbor.isProcessed) continue const inSearch = toSearch.includes(current) const costToNeighbor = current.G + current.getManhattanDistanceTo(neighbor) if (!inSearch || costToNeighbor < neighbor.G) { neighbor.setG(costToNeighbor) neighbor.setConnection(current) current.setFrom(neighbor) if (!inSearch) { neighbor.setH(neighbor.getManhattanDistanceTo(endNode)) toSearch.push(neighbor) } } } } return null } function findNeighborsByGridStep( cur: NodeBase, step: number, bboxes: any, outside: BBoxType, ) { const neighbors: NodeBase[] = [] const { x, y } = cur const { minX, minY, maxX, maxY } = outside const x1 = x - step const x2 = x + step const y1 = y - step const y2 = y + step // eslint-disable-next-line no-shadow function isValid( cur: NodeBase | Point, neighbor: NodeBase | Point, bboxes: BBoxType[], ) { let flag = !isPointInsideTheBoxes(neighbor, bboxes) && !isPointInsideTheBoxes(cur, bboxes) if (!flag) return false for (const bbox of bboxes) { if ( isSegmentCrossingBBox( [ { x: cur.x, y: cur.y, }, { x: neighbor.x, y: neighbor.y, }, ], bbox, ) ) { flag = false break } } return flag } if (x1 >= minX) { isValid( cur, { x: x1, y, }, bboxes, ) && neighbors.push(new NodeBase(x1, y)) } if (x2 <= maxX) { isValid( cur, { x: x2, y, }, bboxes, ) && neighbors.push(new NodeBase(x2, y)) } if (y1 >= minY) { isValid( cur, { x, y: y1, }, bboxes, ) && neighbors.push(new NodeBase(x, y1)) } if (y2 <= maxY) { isValid( cur, { x, y: y2, }, bboxes, ) && neighbors.push(new NodeBase(x, y2)) } return neighbors } function getAnchorWithOffset({ bbox }: any, node: NodeBase, offset: number) { const { minX, minY, maxX, maxY } = bbox const { x, y } = node if (x === minX) { return { x: x - offset, y, } } if (x === maxX) { return { x: x + offset, y, } } if (y === minY) { return { x, y: y - offset, } } if (y === maxY) { return { x, y: y + offset, } } } function perpendicularDistance( point: NodeBase, lineStart: NodeBase, lineEnd: NodeBase, ) { const { x: x1, y: y1 } = lineStart const { x: x2, y: y2 } = lineEnd const { x, y } = point if (x1 === x2) { // 线段是垂直的 return Math.abs(x - x1) } if (y1 === y2) { // 线段是水平的 return Math.abs(y - y1) } // 计算点到线段垂直点的坐标 const px = x1 + ((x2 - x1) * ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1))) / ((x2 - x1) ** 2 + (y2 - y1) ** 2) const py = y1 + ((y2 - y1) * ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1))) / ((x2 - x1) ** 2 + (y2 - y1) ** 2) // 计算曼哈顿距离 return Math.abs(x - px) + Math.abs(y - py) } function perpendicularToStraight(line: string | any[]) { // Step 1: Convert perpendicular segments to straight lines const straightLine = [line[0]] for (let i = 0; i < line.length - 2; i++) { const point1 = line[i] const point2 = line[i + 1] const point3 = line[i + 2] if ( isVertical(point1, point2, point3) || isHorizontal(point1, point2, point3) ) { // Remove point2 to make it a straight line continue } straightLine.push(point2) } straightLine.push(line[line.length - 1]) // Step 2: Douglas-Peucker algorithm to remove redundant points // return straightLine; const epsilon = 1.0 // Adjust epsilon based on your requirements return douglasPeucker(straightLine, epsilon) } function isVertical(p1: { x: any }, p2: { x: any }, p3: { x: any }) { return p1.x === p2.x && p2.x === p3.x } function isHorizontal(p1: { y: any }, p2: { y: any }, p3: { y: any }) { return p1.y === p2.y && p2.y === p3.y } function douglasPeucker(points: string | any[], epsilon: number) { let dmax = 0 let index = 0 for (let i = 1; i < points.length - 1; i++) { const d = perpendicularDistance( points[i], points[0], points[points.length - 1], ) if (d > dmax) { index = i dmax = d } } if (dmax > epsilon) { const left = douglasPeucker(points.slice(0, index + 1), epsilon) const right = douglasPeucker(points.slice(index), epsilon) return left.slice(0, left.length - 1).concat(right) } return [points[0], points[points.length - 1]] } // 每三个点如果其横坐标或者纵坐标都相同,则取其二 function getSimplePath(path: string | any[]) { // if (path.length < 5) return path; path = circleDetection(path) const res: NodeBase[] = [] for (let i = 0; i < path.length; ) { const point1 = path[i] const point2 = path[i + 1] const point3 = path[i + 2] if (!point3) { res.push(point1) i++ continue } if ( (point1.x === point2.x && point2.x === point3.x) || (point1.y === point2.y && point2.y === point3.y) ) { res.push(point1) res.push(point3) i += 3 } else { res.push(point1) i++ } } return res } // 回环检测 & 处理 function circleDetection(path: string | any[]) { if (path.length < 6) return path const res: Array<NodeBase | Point> = [] for (let i = 0; i < path.length; ) { const point1 = path[i] const point2 = path[i + 1] const point4 = path[i + 3] const point5 = path[i + 4] if (!point5) { res.push(point1) i++ continue } if (isSegmentsIntersected([point1, point2], [point4, point5])) { let x = 0 let y = 0 if (point1.x === point2.x) { x = point1.x y = point4.y } else { x = point4.x y = point1.y } res.push({ x, y, }) res.push(point5) i += 4 continue } res.push(point1) i++ } return res } export function getOrient(start: NodeBase, end: NodeBase) { const { x: x1, y: y1 } = start const { x: x2, y: y2 } = end let prefix = '' let suffix = '' if (x1 >= x2) { prefix = 'left' } else { prefix = 'right' } if (y1 >= y2) { suffix = 'top' } else { suffix = 'bottom' } return `${prefix}:${suffix}` } export function ManhattanLayout( startAnchor: any, endAnchor: any, startNode: { bbox: any }, endNode: { bbox: any }, // obstacles, offset: any, ) { // get expanded bbox const { bbox: startBBox } = startNode const { bbox: endBBox } = endNode const startExpandBBox = expandBBox(startNode.bbox, offset) const endExpandBBox = expandBBox(endNode.bbox, offset) // get points from bbox border const points1 = getPointsFromBBoxBorder(startExpandBBox) const points2 = getPointsFromBBoxBorder(endExpandBBox) // is bbox overlap // const overlap = isBBoxOverlap(startBBox, endBBox); const outsideBBox = getHull([...points1, ...points2]) const sNode = getAnchorWithOffset(startNode, startAnchor, offset) const eNode = getAnchorWithOffset(endNode, endAnchor, offset) const sNodeBase = new NodeBase(sNode!.x, sNode!.y) const eNodeBase = new NodeBase(eNode!.x, eNode!.y) const path = aStarFindPathByGrid( eNodeBase, sNodeBase, 10, // obstacles, [startBBox, endBBox], // [startExpandBBox, endExpandBBox], outsideBBox, ) if (path) { const simplifiedPath = perpendicularToStraight(path) return getSimplePath([endAnchor, ...simplifiedPath, startAnchor].reverse()) } }