@logicflow/core
Version:
LogicFlow, help you quickly create flowcharts
607 lines (557 loc) • 18.1 kB
text/typescript
import { ResizeControl, ResizeControlIndex } from '../view/Control'
import { cloneDeep, find, forEach } from 'lodash-es'
import { BaseNodeModel, GraphModel } from '../model'
import { EventType } from '../constant'
import ResizeInfo = ResizeControl.ResizeInfo
import ResizeNodeData = ResizeControl.ResizeNodeData
import {
calculatePointAfterRotateAngle,
getNewCenter,
radianToAngle,
} from '../algorithm/rotate'
import type { SimplePoint } from '../algorithm/rotate'
export function calculateWidthAndHeight(
startRotatedTouchControlPoint: SimplePoint,
endRotatedTouchControlPoint: SimplePoint,
oldCenter: SimplePoint,
angle: number,
freezeWidth = false,
freezeHeight = false,
oldWidth: number,
oldHeight: number,
) {
// 假设目前触摸的是右下角的control点
// 计算出来左上角的control坐标,resize过程左上角的control坐标保持不变
const freezePoint: SimplePoint = {
x: oldCenter.x - (startRotatedTouchControlPoint.x - oldCenter.x),
y: oldCenter.y - (startRotatedTouchControlPoint.y - oldCenter.y),
}
// 【touchEndPoint】右下角 + freezePoint左上角 计算出新的中心点
const newCenter = getNewCenter(freezePoint, endRotatedTouchControlPoint)
// 得到【touchEndPoint】右下角-没有transform的坐标
let endZeroTouchControlPoint: SimplePoint = calculatePointAfterRotateAngle(
endRotatedTouchControlPoint,
newCenter,
-angle,
)
// ---------- 使用transform之前的坐标计算出新的width和height ----------
// 得到左上角---没有transform的坐标
let zeroFreezePoint: SimplePoint = calculatePointAfterRotateAngle(
freezePoint,
newCenter,
-angle,
)
if (freezeWidth) {
// 如果固定width,那么不能单纯使用endZeroTouchControlPoint.x=startZeroTouchControlPoint.x
// 因为去掉transform的左上角不一定是重合的,我们要保证的是transform后的左上角重合
const newWidth = Math.abs(endZeroTouchControlPoint.x - zeroFreezePoint.x)
const widthDx = newWidth - oldWidth
// 点击的是左边锚点,是+widthDx/2,点击是右边锚点,是-widthDx/2
if (newCenter.x > endZeroTouchControlPoint.x) {
// 当前触摸的是左边锚点
newCenter.x = newCenter.x + widthDx / 2
} else {
// 当前触摸的是右边锚点
newCenter.x = newCenter.x - widthDx / 2
}
}
if (freezeHeight) {
const newHeight = Math.abs(endZeroTouchControlPoint.y - zeroFreezePoint.y)
const heightDy = newHeight - oldHeight
if (newCenter.y > endZeroTouchControlPoint.y) {
// 当前触摸的是上边锚点
newCenter.y = newCenter.y + heightDy / 2
} else {
newCenter.y = newCenter.y - heightDy / 2
}
}
if (freezeWidth || freezeHeight) {
// 如果调整过transform之前的坐标,那么transform后的坐标也会改变,那么算出来的newCenter也得调整
// 由于无论如何rotate,中心点都是不变的,因此我们可以使用transform之前的坐标算出新的中心点
const nowFreezePoint = calculatePointAfterRotateAngle(
zeroFreezePoint,
newCenter,
angle,
)
// 得到当前新rect的左上角与实际上transform后的左上角的偏移量
const dx = nowFreezePoint.x - freezePoint.x
const dy = nowFreezePoint.y - freezePoint.y
// 修正不使用transform的坐标: 左上角、右下角、center
newCenter.x = newCenter.x - dx
newCenter.y = newCenter.y - dy
zeroFreezePoint = calculatePointAfterRotateAngle(
freezePoint,
newCenter,
-angle,
)
endZeroTouchControlPoint = {
x: newCenter.x - (zeroFreezePoint.x - newCenter.x),
y: newCenter.y - (zeroFreezePoint.y - newCenter.y),
}
}
// transform之前的坐标的左上角+右下角计算出宽度和高度
let width = Math.abs(endZeroTouchControlPoint.x - zeroFreezePoint.x)
let height = Math.abs(endZeroTouchControlPoint.y - zeroFreezePoint.y)
// ---------- 使用transform之前的坐标计算出新的width和height ----------
if (freezeWidth) {
// 理论计算出来的width应该等于oldWidth
// 但是有误差,比如oldWidth = 100; newWidth=100.000000000001
// 会在handleResize()限制放大缩小的最大最小范围中被阻止滑动
width = oldWidth
}
if (freezeHeight) {
height = oldHeight
}
return {
width,
height,
center: newCenter,
}
}
function recalcRotatedResizeInfo(
pct: number,
resizeInfo: ResizeInfo,
rotate: number,
controlX: number,
controlY: number,
oldCenterX: number,
oldCenterY: number,
freezeWidth = false,
freezeHeight = false,
) {
// 假设我们触摸的点是右下角的control
const { deltaX, deltaY, width: oldWidth, height: oldHeight } = resizeInfo
const angle = radianToAngle(rotate)
// 右下角的control
const startZeroTouchControlPoint = {
x: controlX, // control锚点的坐标x
y: controlY, // control锚点的坐标y
}
const oldCenter = { x: oldCenterX, y: oldCenterY }
// 右下角的control坐标(transform后的-touchStartPoint)
const startRotatedTouchControlPoint = calculatePointAfterRotateAngle(
startZeroTouchControlPoint,
oldCenter,
angle,
)
// 右下角的control坐标(transform后的-touchEndPoint)
const endRotatedTouchControlPoint = {
x: startRotatedTouchControlPoint.x + deltaX,
y: startRotatedTouchControlPoint.y + deltaY,
}
// 计算出新的宽度和高度以及新的中心点
const {
width: newWidth,
height: newHeight,
center: newCenter,
} = calculateWidthAndHeight(
startRotatedTouchControlPoint,
endRotatedTouchControlPoint,
oldCenter,
angle,
freezeWidth,
freezeHeight,
oldWidth,
oldHeight,
)
// calculateWidthAndHeight()得到的是整个宽度,比如圆pct=0.5,此时newWidth等于整个圆直径
resizeInfo.width = newWidth * pct
resizeInfo.height = newHeight * pct
// BaseNodeModel.resize(deltaX/2, deltaY/2),因此这里要*2
resizeInfo.deltaX = (newCenter.x - oldCenter.x) * 2
resizeInfo.deltaY = (newCenter.y - oldCenter.y) * 2
return resizeInfo
}
/**
* 计算 Control 拖动后,节点的高度信息
* @param index
* @param resizeInfo
* @param pct
* @param freezeWidth
* @param freezeHeight
*/
export const recalcResizeInfo = (
index: ResizeControlIndex,
resizeInfo: ResizeInfo,
pct = 1,
freezeWidth = false,
freezeHeight = false,
rotate = 0,
controlX: number | undefined,
controlY: number | undefined,
oldCenterX: number,
oldCenterY: number,
forceProportional = false,
): ResizeInfo => {
const nextResizeInfo = cloneDeep(resizeInfo)
let { deltaX, deltaY } = nextResizeInfo
const { width, height, PCTResizeInfo } = nextResizeInfo
if (PCTResizeInfo) {
const sensitivity = 4 // 越低越灵敏
let deltaScale = 0
let combineDelta = 0
switch (index) {
case ResizeControlIndex.LEFT_TOP:
combineDelta = (deltaX * -1 - deltaY) / sensitivity
break
case ResizeControlIndex.RIGHT_TOP:
combineDelta = (deltaX - deltaY) / sensitivity
break
case ResizeControlIndex.RIGHT_BOTTOM:
combineDelta = (deltaX + deltaY) / sensitivity
break
case ResizeControlIndex.LEFT_BOTTOM:
combineDelta = (deltaX * -1 + deltaY) / sensitivity
break
default:
break
}
if (combineDelta !== 0) {
deltaScale =
Math.round(
(combineDelta / PCTResizeInfo.ResizeBasis.basisHeight) * 100000,
) / 1000
}
PCTResizeInfo.ResizePCT.widthPCT = Math.max(
Math.min(
PCTResizeInfo.ResizePCT.widthPCT + deltaScale,
PCTResizeInfo.ScaleLimit.maxScaleLimit,
),
PCTResizeInfo.ScaleLimit.minScaleLimit,
)
PCTResizeInfo.ResizePCT.heightPCT = Math.max(
Math.min(
PCTResizeInfo.ResizePCT.heightPCT + deltaScale,
PCTResizeInfo.ScaleLimit.maxScaleLimit,
),
PCTResizeInfo.ScaleLimit.minScaleLimit,
)
const spcWidth = Math.round(
(PCTResizeInfo.ResizePCT.widthPCT *
PCTResizeInfo.ResizeBasis.basisWidth) /
100,
)
const spcHeight = Math.round(
(PCTResizeInfo.ResizePCT.heightPCT *
PCTResizeInfo.ResizeBasis.basisHeight) /
100,
)
switch (index) {
case ResizeControlIndex.LEFT_TOP:
deltaX = width - spcWidth
deltaY = height - spcHeight
break
case ResizeControlIndex.RIGHT_TOP:
deltaX = spcWidth - width
deltaY = height - spcHeight
break
case ResizeControlIndex.RIGHT_BOTTOM:
deltaX = spcWidth - width
deltaY = spcHeight - height
break
case ResizeControlIndex.LEFT_BOTTOM:
deltaX = width - spcWidth
deltaY = spcHeight - height
break
default:
break
}
return nextResizeInfo
}
if (
rotate % (2 * Math.PI) !== 0 &&
controlX !== undefined &&
controlY !== undefined
) {
// 角度rotate不为0,则触发另外的计算修正resize的deltaX和deltaY
// 因为rotate不为0的时候,左上角的坐标一直在变化
// 角度rotate不为0得到的resizeInfo.deltaX仅仅代表中心点的变化,而不是宽度的变化
return recalcRotatedResizeInfo(
pct,
nextResizeInfo,
rotate,
controlX,
controlY,
oldCenterX,
oldCenterY,
freezeWidth,
freezeHeight,
)
}
//Shift键等比缩放逻辑
if (forceProportional) {
// 计算当前的宽高比
const aspectRatio = width / height
// 根据拖拽方向确定主要的缩放参考
let primaryDelta = 0
let newWidth = width
let newHeight = height
switch (index) {
case ResizeControlIndex.LEFT_TOP:
// 取绝对值较大的delta作为主要缩放参考
primaryDelta = Math.abs(deltaX) > Math.abs(deltaY) ? -deltaX : -deltaY
if (aspectRatio >= 1) {
// 宽度大于等于高度,以宽度为基准
newWidth = width + primaryDelta
newHeight = newWidth / aspectRatio
} else {
// 高度大于宽度,以高度为基准
newHeight = height + primaryDelta
newWidth = newHeight * aspectRatio
}
nextResizeInfo.width = newWidth
nextResizeInfo.height = newHeight
nextResizeInfo.deltaX = width - newWidth
nextResizeInfo.deltaY = height - newHeight
break
case ResizeControlIndex.RIGHT_TOP:
primaryDelta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : -deltaY
if (aspectRatio >= 1) {
newWidth = width + primaryDelta
newHeight = newWidth / aspectRatio
} else {
newHeight = height - primaryDelta
newWidth = newHeight * aspectRatio
}
nextResizeInfo.width = newWidth
nextResizeInfo.height = newHeight
nextResizeInfo.deltaX = newWidth - width
nextResizeInfo.deltaY = height - newHeight
break
case ResizeControlIndex.RIGHT_BOTTOM:
primaryDelta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY
if (aspectRatio >= 1) {
newWidth = width + primaryDelta
newHeight = newWidth / aspectRatio
} else {
newHeight = height + primaryDelta
newWidth = newHeight * aspectRatio
}
nextResizeInfo.width = newWidth
nextResizeInfo.height = newHeight
nextResizeInfo.deltaX = newWidth - width
nextResizeInfo.deltaY = newHeight - height
break
case ResizeControlIndex.LEFT_BOTTOM:
primaryDelta = Math.abs(deltaX) > Math.abs(deltaY) ? -deltaX : deltaY
if (aspectRatio >= 1) {
newWidth = width - primaryDelta
newHeight = newWidth / aspectRatio
} else {
newHeight = height + primaryDelta
newWidth = newHeight * aspectRatio
}
nextResizeInfo.width = newWidth
nextResizeInfo.height = newHeight
nextResizeInfo.deltaX = width - newWidth
nextResizeInfo.deltaY = newHeight - height
break
default:
break
}
return nextResizeInfo
}
// 原有的非等比缩放逻辑保持不变
switch (index) {
case ResizeControlIndex.LEFT_TOP:
nextResizeInfo.width = freezeWidth ? width : width - deltaX * pct
nextResizeInfo.height = freezeHeight ? height : height - deltaY * pct
break
case ResizeControlIndex.RIGHT_TOP:
nextResizeInfo.width = freezeWidth ? width : width + deltaX * pct
nextResizeInfo.height = freezeHeight ? height : height - deltaY * pct
break
case ResizeControlIndex.RIGHT_BOTTOM:
nextResizeInfo.width = freezeWidth ? width : width + deltaX * pct
nextResizeInfo.height = freezeHeight ? height : height + deltaY * pct
break
case ResizeControlIndex.LEFT_BOTTOM:
nextResizeInfo.width = freezeWidth ? width : width - deltaX * pct
nextResizeInfo.height = freezeHeight ? height : height + deltaY * pct
break
default:
break
}
return nextResizeInfo
}
export const updateEdgePointByAnchors = (
nodeModel: BaseNodeModel,
graphModel: GraphModel,
) => {
// https://github.com/didi/LogicFlow/issues/807
// https://github.com/didi/LogicFlow/issues/875
// 之前的做法,比如Rect是使用getRectResizeEdgePoint()计算边的point缩放后的位置
// getRectResizeEdgePoint()考虑了瞄点在四条边以及在4个圆角的情况
// 使用的是一种等比例缩放的模式,比如:
// const pct = (y - beforeNode.y) / (beforeNode.height / 2 - radius)
// afterPoint.y = afterNode.y + (afterNode.height / 2 - radius) * pct
// 但是用户自定义的getDefaultAnchor()不一定是按照比例编写的
// 它可能是 x: x + 20:每次缩放都会保持在x右边20的位置,因此用户自定义瞄点时,然后产生无法跟随的问题
// 现在的做法是:直接获取用户自定义瞄点的位置,然后用这个位置作为边的新的起点,而不是自己进行计算
const { id, anchors } = nodeModel
const edges = graphModel.getNodeEdges(id)
// 更新边
forEach(edges, (edge) => {
if (edge.sourceNodeId === id) {
// 边是以该节点为 sourceNode 时
const anchorItem = find(
anchors,
(anchor) => anchor.id === edge.sourceAnchorId,
)
if (anchorItem) {
edge.updateStartPoint({
x: anchorItem.x,
y: anchorItem.y,
})
}
} else if (edge.targetNodeId === id) {
// 边是以该节点为 targetNode 时
const anchorItem = find(
anchors,
(anchor) => anchor.id === edge.targetAnchorId,
)
if (anchorItem) {
edge.updateEndPoint({
x: anchorItem.x,
y: anchorItem.y,
})
}
}
})
}
export const triggerResizeEvent = (
preNodeData: ResizeNodeData,
curNodeData: ResizeNodeData,
deltaX: number,
deltaY: number,
index: number,
nodeModel: BaseNodeModel,
graphModel: GraphModel,
) => {
graphModel.eventCenter.emit(EventType.NODE_RESIZE, {
preData: preNodeData,
data: curNodeData,
deltaX,
deltaY,
index,
model: nodeModel,
})
}
// TODO:确认 handleResize 函数的类型定义
export type IHandleResizeParams = {
x?: number
y?: number
deltaX: number
deltaY: number
index: ResizeControlIndex
nodeModel: BaseNodeModel
graphModel: GraphModel
cancelCallback?: () => void
forceProportional?: boolean
}
/**
* 处理节点的 resize 事件,提出来放到 utils 中,方便在外面(extension)中使用
* @param x
* @param y
* @param deltaX
* @param deltaY
* @param index
* @param nodeModel
* @param graphModel
* @param cancelCallback
* @param forceProportional
*/
export const handleResize = ({
x,
y,
deltaX,
deltaY,
index,
nodeModel,
graphModel,
cancelCallback,
forceProportional = false,
}: IHandleResizeParams) => {
const {
r, // circle
rx, // ellipse/diamond
ry,
width, // rect/html
height,
PCTResizeInfo,
minWidth,
minHeight,
maxWidth,
maxHeight,
rotate,
x: oldCenterX,
y: oldCenterY,
} = nodeModel
const isFreezeWidth = minWidth === maxWidth
const isFreezeHeight = minHeight === maxHeight
const resizeInfo = {
width: r || rx || width,
height: r || ry || height,
deltaX,
deltaY,
PCTResizeInfo,
}
const pct = r || (rx && ry) ? 1 / 2 : 1
const controlX = x
const controlY = y
const nextSize = recalcResizeInfo(
index,
resizeInfo,
pct,
isFreezeWidth,
isFreezeHeight,
rotate,
controlX,
controlY,
oldCenterX,
oldCenterY,
forceProportional,
)
// 限制放大缩小的最大最小范围
if (
nextSize.width < minWidth ||
nextSize.width > maxWidth ||
nextSize.height < minHeight ||
nextSize.height > maxHeight
) {
// this.dragHandler.cancelDrag()
cancelCallback?.()
return
}
if (
rotate % (2 * Math.PI) == 0 ||
PCTResizeInfo ||
controlX === undefined ||
controlY === undefined
) {
// rotate!==0并且不是PCTResizeInfo时,即使是isFreezeWidth||isFreezeHeight
// recalcRotatedResizeInfo()计算出来的中心点会发生变化
// 如果限制了宽高不变,对应的 x/y 不产生位移
nextSize.deltaX = isFreezeWidth ? 0 : nextSize.deltaX
nextSize.deltaY = isFreezeHeight ? 0 : nextSize.deltaY
}
const preNodeData = nodeModel.getData()
const curNodeData = nodeModel.resize(nextSize)
// 检测preNodeData和curNodeData是否没变化
if (preNodeData.x === curNodeData.x && preNodeData.y === curNodeData.y) {
// 中心点x和y都没有变化,说明无法resize,阻止下面边的更新以及resize事件的emit
return
}
// 更新边
updateEdgePointByAnchors(nodeModel, graphModel)
// 触发 resize 事件
triggerResizeEvent(
preNodeData,
curNodeData,
deltaX,
deltaY,
index,
nodeModel,
graphModel,
)
}