@logicflow/core
Version:
LogicFlow, help you quickly create flowcharts
751 lines (683 loc) • 18.8 kB
text/typescript
import { assign, cloneDeep, find, isUndefined } from 'lodash-es'
import { action, computed, isObservable, observable, set, toJS } from 'mobx'
import { BaseNodeModel, GraphModel, Model } from '..'
import LogicFlow from '../../LogicFlow'
import {
createUuid,
formatData,
getAnchors,
getZIndex,
pickEdgeConfig,
twoPointDistance,
} from '../../util'
import {
ElementState,
ElementType,
ModelType,
OverlapMode,
TextMode,
} from '../../constant'
import Point = LogicFlow.Point
import EdgeData = LogicFlow.EdgeData
import EdgeConfig = LogicFlow.EdgeConfig
import TextConfig = LogicFlow.TextConfig
import PropertiesType = LogicFlow.PropertiesType
export interface IBaseEdgeModel<P extends PropertiesType>
extends Model.BaseModel<P> {
/**
* model 基础类型,固定为 edge
*/
readonly BaseType: ElementType.EDGE
properties: P
sourceNodeId: string
targetNodeId: string
startPoint?: Point
endPoint?: Point
points: string
pointsList: Point[]
isAnimation: boolean
isDragging?: boolean
isShowAdjustPoint: boolean // 是否显示边两端的调整点
sourceAnchorId?: string
targetAnchorId?: string
arrowConfig?: LogicFlow.ArrowConfig
}
export class BaseEdgeModel<P extends PropertiesType = PropertiesType>
implements IBaseEdgeModel<P>
{
readonly BaseType = ElementType.EDGE
static BaseType: ElementType = ElementType.EDGE
// 数据属性
public id = ''
type = ''
sourceNodeId = ''
targetNodeId = ''
startPoint!: Point
endPoint!: Point
textMode = TextMode.TEXT
text: Required<TextConfig> = {
value: '',
x: 0,
y: 0,
draggable: false,
editable: true,
}
properties: P
points = ''
pointsList: Point[] = []
// 状态属性
virtual = false
isSelected = false
isHovered = false
isHitable = true // 细粒度控制边是否对用户操作进行反应
isHittable = true // 细粒度控制边是否对用户操作进行反应
draggable = true
visible = true
// 边特有属性,动画及调整点
isAnimation = false
isShowAdjustPoint = false // 是否显示边两端的调整点
isDragging?: boolean
// 引用属性
graphModel: GraphModel
zIndex: number = 0
state = ElementState.DEFAULT
modelType = ModelType.EDGE
additionStateData?: Model.AdditionStateDataType
sourceAnchorId?: string
targetAnchorId?: string
menu?: LogicFlow.MenuConfig[]
customTextPosition = false // 是否自定义边文本位置
style: LogicFlow.CommonTheme = {} // 每条边自己的样式,动态修改
// TODO: 每个边独立生成一个marker没必要
// 箭头属性
arrowConfig: LogicFlow.ArrowConfig = {
markerEnd: `url(#marker-end-${this.id})`,
markerStart: `url(#marker-start-${this.id})`,
};
[propName: string]: unknown // 支持自定义
constructor(data: EdgeConfig<P>, graphModel: GraphModel) {
this.graphModel = graphModel
this.properties = data.properties ?? ({} as P)
this.initEdgeData(data)
this.setAttributes()
}
/**
* 初始化边数据
* @overridable 支持重写
* initNodeData和setAttributes的区别在于
* initNodeData只在节点初始化的时候调用,用于初始化节点的所有属性。
* setAttributes除了初始化调用外,还会在properties发生变化后调用。
*/
initEdgeData(data: EdgeConfig) {
if (!data.properties) {
data.properties = {}
}
if (!data.id) {
// 自定义边id > 全局定义边id > 内置
const { idGenerator } = this.graphModel
const globalId = idGenerator && idGenerator(data.type)
const nodeId = this.createId()
data.id = nodeId || globalId || createUuid()
}
this.arrowConfig.markerEnd = `url(#marker-end-${data.id})`
this.arrowConfig.markerStart = `url(#marker-start-${data.id})`
const {
editConfigModel: { adjustEdgeStartAndEnd },
} = this.graphModel
this.isShowAdjustPoint = adjustEdgeStartAndEnd
assign(this, pickEdgeConfig(data))
const { overlapMode } = this.graphModel
if (overlapMode === OverlapMode.INCREASE) {
this.zIndex = data.zIndex || getZIndex()
}
// 设置边的 anchors,也就是边的两个端点
// 端点依赖于 edgeData 的 sourceNode 和 targetNode
this.setAnchors()
// 边的拐点依赖于两个端点
this.initPoints()
// 文本位置依赖于边上的所有拐点
this.formatText(data)
}
/**
* 设置model属性
* @overridable 支持重写
* 每次properties发生变化会触发
*/
setAttributes() {}
createId(): string | null {
return null
}
/**
* 自定义边样式
*
* @overridable 支持重写
* @returns 自定义边样式
*/
getEdgeStyle(): LogicFlow.EdgeTheme {
return {
...this.graphModel.theme.baseEdge,
...this.style,
}
}
/**
* 自定义边调整点样式
*
* @overridable 支持重写
* 在isShowAdjustPoint为true时会显示调整点。
*/
getAdjustPointStyle() {
return {
...this.graphModel.theme.edgeAdjust,
}
}
/**
* 自定义边文本样式
*
* @overridable 支持重写
*/
getTextStyle() {
// 透传 edgeText
const { edgeText } = this.graphModel.theme
return cloneDeep(edgeText)
}
/**
* 自定义边动画样式
*
* @overridable 支持重写
* @example
* getEdgeAnimationStyle() {
* const style = super.getEdgeAnimationStyle()
* style.stroke = 'blue'
* style.animationDuration = '30s'
* style.animationDirection = 'reverse'
* return style
* }
*/
getEdgeAnimationStyle() {
const { edgeAnimation } = this.graphModel.theme
return cloneDeep(edgeAnimation)
}
/**
* 自定义边箭头样式
*
* @overridable 支持重写
* @example
* getArrowStyle() {
* const style = super.getArrowStyle()
* style.stroke = 'green'
* return style
* }
*/
getArrowStyle(): LogicFlow.ArrowTheme {
const edgeStyle = this.getEdgeStyle()
const edgeAnimationStyle = this.getEdgeAnimationStyle()
const { arrow } = this.graphModel.theme
const stroke = this.isAnimation
? edgeAnimationStyle.stroke
: edgeStyle.stroke
return {
...edgeStyle,
fill: stroke,
stroke,
...arrow,
}
}
/**
* 自定义边被选中时展示其范围的矩形框样式
*
* @overridable 支持重写
* @example
* // 隐藏outline
* getOutlineStyle() {
* const style = super.getOutlineStyle()
* style.stroke = "none"
* style.hover.stroke = "none"
* return style
* }
*/
getOutlineStyle(): LogicFlow.OutlineTheme {
const { graphModel } = this
const { outline } = graphModel.theme
return cloneDeep(outline)
}
/**
* 重新自定义文本位置
*
* @overridable 支持重写
*/
getTextPosition(): Point {
return {
x: 0,
y: 0,
}
}
/**
* 边的前一个节点
*/
get sourceNode() {
return this.graphModel?.nodesMap[this.sourceNodeId]?.model
}
/**
* 边的后一个节点
*/
get targetNode() {
return this.graphModel?.nodesMap[this.targetNodeId]?.model
}
get textPosition(): Point {
return this.getTextPosition()
}
/**
* 内部方法,计算两个节点相连时的起点位置
*/
getBeginAnchor(
sourceNode: BaseNodeModel,
targetNode: BaseNodeModel,
sourceAnchorId?: string,
): Point | undefined {
// https://github.com/didi/LogicFlow/issues/1077
// 可能拿到的sourceAnchors为空数组,因此position可能返回为undefined
let position: Point | undefined
let minDistance: number | undefined
const sourceAnchors = getAnchors(sourceNode)
if (sourceAnchorId) {
position = find(sourceAnchors, (anchor) => anchor.id === sourceAnchorId)
// 如果指定了起始锚点,且指定锚点是节点拥有的锚点时,就把该点设置为起点
if (position) {
return position
}
console.warn(
`未在节点上找到指定的起点锚点${sourceAnchorId},已使用默认锚点作为起点`,
)
}
sourceAnchors.forEach((anchor) => {
const distance = twoPointDistance(anchor, targetNode)
if (minDistance === undefined) {
minDistance = distance
position = anchor
} else if (distance < minDistance) {
minDistance = distance
position = anchor
}
})
return position
}
/**
* 内部方法,计算两个节点相连时的终点位置
*/
getEndAnchor(
targetNode: BaseNodeModel,
targetAnchorId?: string,
): Point | undefined {
// https://github.com/didi/LogicFlow/issues/1077
// 可能拿到的targetAnchors为空数组,因此position可能返回为undefined
let position: Point | undefined
let minDistance: number | undefined
const targetAnchors = getAnchors(targetNode)
if (targetAnchorId) {
position = find(targetAnchors, (anchor) => anchor.id === targetAnchorId)
// 如果指定了终点锚点,且指定锚点是节点拥有的锚点时,就把该点设置为终点
if (position) {
return position
}
console.warn(
`未在节点上找到指定的终点锚点${targetAnchorId},已使用默认锚点作为终点`,
)
}
targetAnchors.forEach((anchor) => {
if (!this.startPoint) return // 如果此时 this.startPoint 为 undefined,直接返回
const distance = twoPointDistance(anchor, this.startPoint)
if (minDistance === undefined) {
minDistance = distance
position = anchor
} else if (distance < minDistance) {
minDistance = distance
position = anchor
}
})
return position
}
/**
* 获取当前边的properties
*/
getProperties() {
return toJS(this.properties)
}
/**
* 获取被保存时返回的数据
*
* @overridable 支持重写
*/
getData(): EdgeData {
let { properties } = this
if (isObservable(properties)) {
properties = toJS(properties)
}
const data: EdgeData = {
id: this.id,
type: this.type,
properties,
sourceNodeId: this.sourceNodeId,
targetNodeId: this.targetNodeId,
sourceAnchorId: this.sourceAnchorId,
targetAnchorId: this.targetAnchorId,
startPoint: assign({}, this.startPoint),
endPoint: assign({}, this.endPoint),
}
if (this.graphModel.overlapMode === OverlapMode.INCREASE) {
data.zIndex = this.zIndex
}
const { x, y, value } = this.text
if (value) {
data.text = {
x,
y,
value,
}
}
return data
}
/**
* 获取边的数据
*
* @overridable 支持重写
* 用于在历史记录时获取节点数据。
* 在某些情况下,如果希望某个属性变化不引起history的变化,
* 可以重写此方法。
*/
getHistoryData(): EdgeData {
return this.getData()
}
/**
* 设置边的属性,会触发重新渲染
* @param key 属性名
* @param val 属性值
*/
setProperty(key: string, val: any): void {
set(this.properties, key, formatData(val))
this.setAttributes()
}
/**
* 删除边的属性,会触发重新渲染
* @param key 属性名
*/
deleteProperty(key: string): void {
delete this.properties[key]
this.setAttributes()
}
/**
* 设置边的属性,会触发重新渲染
* @param properties 要更新的 properties,会做合并
*/
setProperties(properties: Record<string, any>): void {
this.properties = {
...toJS(this.properties),
...formatData(properties),
}
this.setAttributes()
}
/**
* 修改边的id
*/
changeEdgeId(id: string) {
const { markerEnd, markerStart } = this.arrowConfig
if (markerStart && markerStart === `url(#marker-start-${this.id})`) {
this.arrowConfig.markerStart = `url(#marker-start-${id})`
}
if (markerEnd && markerEnd === `url(#marker-end-${this.id})`) {
this.arrowConfig.markerEnd = `url(#marker-end-${id})`
}
this.id = id
}
/**
* 设置边样式,用于插件开发时跳过自定义边的渲染。大多数情况下,不需要使用此方法。
* 如果需要设置边的样式,请使用 getEdgeStyle 方法自定义边样式。
*/
setStyle(key: string, val): void {
this.style = {
...this.style,
[key]: formatData(val),
}
}
/**
* 设置边样式,用于插件开发时跳过自定义边的渲染。大多数情况下,不需要使用此方法。
* 如果需要设置边的样式,请使用 getEdgeStyle 方法自定义边样式。
*/
setStyles(styles): void {
this.style = {
...this.style,
...formatData(styles),
}
}
/**
* 设置边样式,用于插件开发时跳过自定义边的渲染。大多数情况下,不需要使用此方法。
* 如果需要设置边的样式,请使用 getEdgeStyle 方法自定义边样式。
*/
updateStyles(styles): void {
this.style = {
...formatData(styles),
}
}
/**
* 设置当前元素的文本模式
* @param mode
*/
setTextMode(mode: TextMode) {
this.textMode = mode
}
/**
* 内部方法,处理初始化文本格式
*/
formatText(data: EdgeConfig) {
const {
editConfigModel: { edgeTextDraggable, edgeTextEdit },
} = this.graphModel
const { x, y } = this.textPosition
const { text } = data
let textConfig: Required<TextConfig> = {
value: '',
x,
y,
draggable: edgeTextDraggable,
editable: edgeTextEdit,
}
if (text) {
if (typeof text === 'string') {
textConfig = {
...textConfig,
value: text,
}
} else {
textConfig = {
...textConfig,
x: text.x ?? x,
y: text.y ?? y,
value: text.value ?? '',
}
if (!isUndefined(text.draggable)) {
textConfig.draggable = text.draggable
}
if (!isUndefined(text.editable)) {
textConfig.editable = text.editable
}
}
}
this.text = textConfig
}
/**
* 重置文本位置
*/
resetTextPosition() {
const { x, y } = this.textPosition
this.text.x = x
this.text.y = y
}
/**
* 移动边上的文本
*/
moveText(deltaX: number, deltaY: number): void {
const { x, y, value, draggable, editable } = this.text
this.text = {
value,
editable,
draggable,
x: x + deltaX,
y: y + deltaY,
}
}
/**
* 设置文本位置和值
*/
setText(textConfig: LogicFlow.TextConfig): void {
if (textConfig) {
assign(this.text, textConfig)
}
}
/**
* 更新文本的值
*/
updateText(value: string): void {
this.text = {
...toJS(this.text),
value,
}
}
/**
* 内部方法,计算边的起点和终点和其对于的锚点Id
*/
setAnchors(): void {
if (!this.sourceAnchorId || !this.startPoint) {
const anchor = this.getBeginAnchor(
this.sourceNode,
this.targetNode,
this.sourceAnchorId,
)
if (!anchor) {
// https://github.com/didi/LogicFlow/issues/1077
// 当用户自定义getDefaultAnchor(){return []}时,表示:不显示锚点,也不允许其他节点连接到此节点
// 此时拿到的anchor=undefined,下面会直接报错
throw new Error(
'无法获取beginAnchor,请检查anchors相关逻辑,anchors不能为空',
)
}
if (!this.startPoint) {
this.startPoint = {
x: anchor.x,
y: anchor.y,
}
}
if (!this.sourceAnchorId) {
this.sourceAnchorId = anchor.id
}
}
if (!this.targetAnchorId || !this.endPoint) {
const anchor = this.getEndAnchor(this.targetNode, this.targetAnchorId)
if (!anchor) {
// https://github.com/didi/LogicFlow/issues/1077
// 当用户自定义getDefaultAnchor(){return []}时,表示:不显示锚点,也不允许其他节点连接到此节点
// 此时拿到的anchor=undefined,下面会直接报错
throw new Error(
'无法获取endAnchor,请检查anchors相关逻辑,anchors不能为空',
)
}
if (!this.endPoint) {
this.endPoint = {
x: anchor.x,
y: anchor.y,
}
}
if (!this.targetAnchorId) {
this.targetAnchorId = anchor.id
}
}
}
setSelected(flag = true): void {
this.isSelected = flag
}
setHovered(flag = true): void {
this.isHovered = flag
}
setHitable(flag = true): void {
this.isHitable = flag
}
setHittable(flag = true): void {
this.isHittable = flag
}
openEdgeAnimation(): void {
this.isAnimation = true
}
closeEdgeAnimation(): void {
this.isAnimation = false
}
setElementState(
state: ElementState,
additionStateData?: Model.AdditionStateDataType,
): void {
this.state = state
this.additionStateData = additionStateData
}
updateStartPoint(anchor: Point): void {
this.startPoint = anchor
}
moveStartPoint(deltaX: number, deltaY: number): void {
if (this.startPoint) {
this.startPoint.x += deltaX
this.startPoint.y += deltaY
}
}
updateEndPoint(anchor: Point): void {
this.endPoint = anchor
}
moveEndPoint(deltaX: number, deltaY: number): void {
if (this.endPoint) {
this.endPoint.x += deltaX
this.endPoint.y += deltaY
}
}
setZIndex(zIndex = 0): void {
this.zIndex = zIndex
}
initPoints() {}
updateAttributes(attributes) {
assign(this, attributes)
}
// 获取边调整的起点
getAdjustStart() {
return this.startPoint
}
// 获取边调整的终点
getAdjustEnd() {
return this.endPoint
}
// 起终点拖拽调整过程中,进行直线路径更新
updateAfterAdjustStartAndEnd({
startPoint,
endPoint,
}: Record<'startPoint' | 'endPoint', Point>) {
this.updateStartPoint({ x: startPoint.x, y: startPoint.y })
this.updateEndPoint({ x: endPoint.x, y: endPoint.y })
}
}
export default BaseEdgeModel