@logicflow/extension
Version:
LogicFlow Extensions
396 lines (358 loc) • 11.5 kB
text/typescript
import LogicFlow, {
BaseEdgeModel,
BaseNodeModel,
getNodeOutline,
getEdgeOutline,
Model,
ModelType,
BezierEdgeModel,
isInSegment,
getBezierPoints,
points2PointsList,
getBBoxOfPoints,
} from '@logicflow/core'
import { head, isEmpty, last, min } from 'lodash-es'
import { calcTwoPointsDistance, getPointOnBezier } from './algorithm'
import Point = LogicFlow.Point
import LabelConfig = LogicFlow.LabelConfig
import OutlineInfo = Model.OutlineInfo
export type BBoxInfo = {
x: number
y: number
width: number
height: number
}
// 工具函数:计算「缩放」后 某坐标点 相对中心位置比例不变的 新坐标点
// 前提条件: 当缩放一个矩形时,如果你希望矩形中的某个点的位置相对于矩形保持不变
//
// 1. 原始矩形的左上角坐标为 (x1, y1),宽度为 w1,高度为 h1。
// 2. 缩放后的矩形的左上角坐标为 (x2, y2),宽度为 w2,高度为 h2。
// 3. 矩形中的某个点在原始矩形中的坐标为 (px1, py1)。
//
// 目标
// 计算该点在缩放后矩形中的新坐标 (px2, py2)。
//
// 步骤
// 1. 计算相对位置:首先计算点 (px1, py1) 在原始矩形中的相对位置。
// relativeX = (px1 - x1) / w1
// relativeY = (py1 - y1) / h1
//
// 2. 计算新坐标:然后,使用相对位置计算该点在缩放后矩形中的新坐标。
// px2 = x2 + relativeX * w2
// py2 = y2 + relativeY * h2
export function calcPointAfterResize(
origin: BBoxInfo,
scaled: BBoxInfo,
point: Point,
): Point {
const { x: x1, y: y1, width: w1, height: h1 } = origin
const { x: x2, y: y2, width: w2, height: h2 } = scaled
const { x: px1, y: py1 } = point
// 计算点在原始矩形中的相对位置
const relativeX = (px1 - x1) / w1
const relativeY = (py1 - y1) / h1
// 计算点在缩放后矩形中的新坐标
const px2 = x2 + relativeX * w2
const py2 = y2 + relativeY * h2
return { x: px2, y: py2 }
}
// 工具函数:计算「旋转」后 某坐标点 相对中心位置比例不变的 新坐标点
// 要计算以点 x1 = (x1, y1) 为中心,点 x2 = (x2, y2) 旋转 θ 度后的坐标位置,可以使用旋转矩阵进行计算。
//
// 旋转公式如下:
// 1. 首先将点 x2 平移到以 x1 为原点的坐标系:
// x' = x2 - x1
// y' = y2 - y1
// 2. 然后应用旋转矩阵进行旋转:
// x'' = x' * cos(θ) - y' * sin(θ)
// y'' = x' * sin(θ) + y' * cos(θ)
// 3. 最后将结果平移回原来的坐标系:
// x_new = x'' + x1
// y_new = y'' + y1
//
// 综合起来,旋转后的新坐标 (x_new, y_new) 计算公式如下:
//
// x_new = (x2 - x1) * cos(θ) - (y2 - y1) * sin(θ) + x1
// y_new = (x2 - x1) * sin(θ) + (y2 - y1) * cos(θ) + y1
//
// 其中,θ 需要用弧度表示,如果你有的是角度,可以用以下公式转换为弧度:
//
// rad = deg * π / 180
export function rotatePointAroundCenter(
target: Point,
center: Point,
radian: number,
): Point {
// Rotate point (x2, y2) around point (x1, y1) by theta degrees.
//
// Parameters:
// x1, y1: Coordinates of the center point.
// x2, y2: Coordinates of the point to rotate.
// theta_degrees: Angle in degrees to rotate the point.
//
// Returns:
// Tuple of new coordinates (x_new, y_new) after rotation.
const { x: x1, y: y1 } = center
const { x: x2, y: y2 } = target
// Translate point to origin
const xPrime = x2 - x1
const yPrime = y2 - y1
// Rotate point
const xDoublePrime = xPrime * Math.cos(radian) - yPrime * Math.sin(radian)
const yDoublePrime = xPrime * Math.sin(radian) + yPrime * Math.cos(radian)
// Translate point back
const xNew = xDoublePrime + x1
const yNew = yDoublePrime + y1
return {
x: xNew,
y: yNew,
}
}
/** Edge 相关工具方法 */
/**
* 获取某点在一个矩形图形(节点 or 边的 outline)内的偏移量
* @param point 目标点(此处即 Label 的坐标信息)
* @param element 目标元素
*/
export const getPointOffsetOfElementOutline = (
point: Point,
element: BaseNodeModel | BaseEdgeModel,
) => {
const baseType = element.BaseType
const bboxInfo: OutlineInfo | undefined =
baseType === 'node' ? getNodeOutline(element) : getEdgeOutline(element)
if (bboxInfo) {
const { x, y } = point
const { x: minX, y: minY, x1: maxX, y1: maxY } = bboxInfo
let xDeltaPercent: number = 0.5
let yDeltaPercent: number = 0.5
let xDeltaDistance: number = x - maxX
let yDeltaDistance: number = y - maxY
/**
* 文本在由路径点组成的凸包内,就记录偏移比例
* 文本在凸包外,记录绝对距离
* 用于边路径变化时计算文本新位置
*/
if (minX && maxX && minX < x && x < maxX) {
xDeltaPercent = min([(x - minX) / (maxX - minX), 1]) as number
} else if (x <= minX) {
xDeltaDistance = x - minX
} else {
xDeltaDistance = x - maxX
}
if (minY && maxY && minY < y && y < maxY) {
yDeltaPercent = min([(y - minY) / (maxY - minY), 1]) as number
} else if (y <= minY) {
yDeltaDistance = y - minY
} else {
yDeltaDistance = y - maxY
}
return {
xDeltaPercent,
yDeltaPercent,
xDeltaDistance,
yDeltaDistance,
}
}
}
/**
* 判断节点是否在折线上
* @param point 目标点坐标
* @param points 折线上的点坐标
*/
const isPointOnPolyline = (point: Point, points: Point[]): boolean => {
for (let i = 0; i < points.length - 1; i++) {
const start = points[i]
const end = points[i + 1]
if (isInSegment(point, start, end)) {
return true
}
}
return false
}
/**
* 给定一个点 P = (x_0, y_0) 和线段的两个端点 A = (x_1, y_1) 和 B = (x_2, y_2) ,可以使用以下步骤计算点到线段的距离:
* 1. 计算向量 AB 和 AP 。
* 2. 计算 AB 的平方长度。
* 3. 计算点 P 在直线 AB 上的投影点 Q 。
* 4. 判断 Q 是否在线段 AB 上。
* 5. 根据 Q 是否在线段上,计算点到线段的距离。
*
* 计算点到线段质检的距离
* @param point
* @param start
* @param end
*/
export const pointToSegmentDistance = (
point: Point,
start: Point,
end: Point,
): number => {
const { x: px, y: py } = point
const { x: sx, y: sy } = start
const { x: ex, y: ey } = end
const SEx = ex - sx
const SEy = ey - sy
const SPx = px - sx
const SPy = py - sy
const SE_SE = SEx ** 2 + SEy ** 2
const SP_SE = SPx * SEx + SPy * SEy
let t = SP_SE / SE_SE
if (t < 0) t = 0
if (t > 1) t = 1
const qx = sx + t * SEx
const qy = sy + t * SEy
return Math.sqrt((px - qx) ** 2 + (py - qy) ** 2)
}
export const calcPolylineTotalLength = (points: Point[]) => {
let length = 0
for (let i = 0; i < points.length - 1; i++) {
const start = points[i]
const end = points[i + 1]
length += calcTwoPointsDistance(start, end)
}
return length
}
/**
* TODO: 确认该函数的意义,写完还是没看懂什么意思
* @param point
* @param points
*/
export const pointPositionRatio = (point: Point, points: Point[]): number => {
let length = 0
for (let i = 0; i < points.length - 1; i++) {
const start = points[i]
const end = points[i + 1]
const segmentLength = calcTwoPointsDistance(start, end)
if (pointToSegmentDistance(point, start, end) <= 20) {
const d1 = calcTwoPointsDistance(point, start)
length += d1
const totalLength = calcPolylineTotalLength(points)
// 小数点后保留一位(四舍五入)
return Math.round((length / totalLength) * 10) / 10
} else {
length += segmentLength
}
}
return 0
}
/**
* 计算一个坐标在贝塞尔曲线上最近的一个点
* @param point
* @param edge
* @param step
*/
export const calcClosestPointOnBezierEdge = (
point: Point,
edge: BezierEdgeModel,
step: number = 5,
): Point => {
let minDistance = Infinity
let closestPoint: Point = point
const pointsList = getBezierPoints(edge.path)
if (isEmpty(pointsList)) return closestPoint
const [start, sNext, ePre, end] = pointsList
for (let i = 0; i <= step; i++) {
const t = i / step
const bezierPoint = getPointOnBezier(t, start, sNext, ePre, end)
const distance = calcTwoPointsDistance(point, bezierPoint)
if (distance < minDistance) {
minDistance = distance
closestPoint = bezierPoint
}
}
return closestPoint
}
export const getNewPointAtDistance = (
points: Point[],
ratio: number,
): Point | undefined => {
const totalLength = calcPolylineTotalLength(points)
const targetLength = totalLength * ratio
let length = 0
for (let i = 0; i < points.length - 1; i++) {
const start = points[i]
const end = points[i + 1]
const segmentLength = calcTwoPointsDistance(start, end)
if (length + segmentLength >= targetLength) {
const ratio = (targetLength - length) / segmentLength
return {
x: start.x + (end.x - start.x) * ratio,
y: start.y + (end.y - start.y) * ratio,
}
} else {
length += segmentLength
}
}
return last(points)
}
/**
* 计算一个坐标离折线(包括 PolylineEdge 和 LineEdge 直线)最近的一个点
* @param point
* @param edge
*/
export const calcLabelPositionOnPolyline = (
point: Point,
edge: BaseEdgeModel,
): Point => {
let points = points2PointsList(edge.points)
if (points.length === 0) {
points = [edge.startPoint, edge.endPoint]
}
const { xDeltaPercent, yDeltaPercent, yDeltaDistance, xDeltaDistance } =
getPointOffsetOfElementOutline(point, edge) ?? {}
const isPointOnEdge = isPointOnPolyline(point, points)
const ratio = pointPositionRatio(point, points)
const start = head(points)
const end = last(points)
// 分别取路径中,x轴 和 y轴上的最大最小坐标值组合成一个矩形
const { minX, minY, maxX, maxY } = getBBoxOfPoints(points, 10)
if (!start || !end) return point
if (xDeltaPercent && yDeltaPercent) {
const positByPercent = {
x: minX + (maxX - minX) * xDeltaPercent,
y: minY + (maxY - minY) * yDeltaPercent,
}
return isPointOnEdge
? (getNewPointAtDistance(points, ratio) ?? point) // 函数什么意思
: positByPercent
}
// 如果文本在凸包的上方或者下方
if (xDeltaPercent && yDeltaDistance) {
return {
x: minX + (maxX - minX) * xDeltaPercent,
y: yDeltaDistance < 0 ? minY + yDeltaDistance : maxY + yDeltaDistance,
}
}
// 如果文本在凸包的左边或者右边
if (yDeltaPercent && xDeltaDistance) {
return {
x: xDeltaDistance < 0 ? minX + xDeltaDistance : maxX + xDeltaDistance,
y: minY + (maxY - minY) * yDeltaPercent,
}
}
// 如果文本在凸包左上/左下/右上/右下
if (xDeltaDistance && yDeltaDistance) {
return {
x: xDeltaDistance < 0 ? minX + xDeltaDistance : maxX + xDeltaDistance,
y: yDeltaDistance < 0 ? minY + yDeltaDistance : maxY + yDeltaDistance,
}
}
// 兜底
return point
}
/**
* 计算 Label 离边最近的点的坐标,用于更新为 Label 的坐标
* @param label LabelConfig -> 当前 Label 的配置项
* @param edge
*/
export const getLabelPositionOfLine = (
label: LabelConfig,
edge: BaseEdgeModel,
): Point => {
const { x, y } = label
if (edge.modelType === ModelType.BEZIER_EDGE) {
return calcClosestPointOnBezierEdge({ x, y }, edge as BezierEdgeModel)
}
return calcLabelPositionOnPolyline({ x, y }, edge)
}