628 lines
18 KiB
JavaScript
628 lines
18 KiB
JavaScript
import { walk, bfsWalk, throttle } from './utils/'
|
||
import { v4 as uuid } from 'uuid'
|
||
import {
|
||
getAssociativeLineTargetIndex,
|
||
computeCubicBezierPathPoints,
|
||
joinCubicBezierPath,
|
||
cubicBezierPath,
|
||
getNodePoint,
|
||
computeNodePoints,
|
||
getNodeLinePath,
|
||
getDefaultControlPointOffsets
|
||
} from './utils/associativeLineUtils'
|
||
|
||
// 关联线类
|
||
class AssociativeLine {
|
||
constructor(opt = {}) {
|
||
this.mindMap = opt.mindMap
|
||
this.draw = this.mindMap.draw
|
||
// 当前所有连接线
|
||
this.lineList = []
|
||
// 当前激活的连接线
|
||
this.activeLine = null
|
||
// 当前正在创建连接线
|
||
this.isCreatingLine = false // 是否正在创建连接线中
|
||
this.creatingStartNode = null // 起始节点
|
||
this.creatingLine = null // 创建过程中的连接线
|
||
this.overlapNode = null // 创建过程中的目标节点
|
||
// 是否有节点正在被拖拽
|
||
this.isNodeDragging = false
|
||
// 箭头图标
|
||
this.markerPath = null
|
||
this.marker = this.createMarker()
|
||
// 控制点
|
||
this.controlLine1 = null
|
||
this.controlLine2 = null
|
||
this.controlPoint1 = null
|
||
this.controlPoint2 = null
|
||
this.controlPointDiameter = 10
|
||
this.isControlPointMousedown = false
|
||
this.mousedownControlPointKey = ''
|
||
this.controlPointMousemoveState = {
|
||
pos: null,
|
||
startPoint: null,
|
||
endPoint: null,
|
||
targetIndex: ''
|
||
}
|
||
// 节流一下,不然很卡
|
||
this.checkOverlapNode = throttle(this.checkOverlapNode, 100, this)
|
||
this.bindEvent()
|
||
}
|
||
|
||
// 监听事件
|
||
bindEvent() {
|
||
// 节点树渲染完毕后渲染连接线
|
||
this.renderAllLines = this.renderAllLines.bind(this)
|
||
this.mindMap.on('node_tree_render_end', this.renderAllLines)
|
||
// 状态改变后重新渲染连接线
|
||
this.mindMap.on('data_change', this.renderAllLines)
|
||
// 监听画布和节点点击事件,用于清除当前激活的连接线
|
||
this.mindMap.on('draw_click', () => {
|
||
if (this.isControlPointMousedown) {
|
||
return
|
||
}
|
||
this.clearActiveLine()
|
||
})
|
||
this.mindMap.on('node_click', node => {
|
||
if (this.isCreatingLine) {
|
||
this.completeCreateLine(node)
|
||
} else {
|
||
this.clearActiveLine()
|
||
}
|
||
})
|
||
// 注册删除快捷键
|
||
this.mindMap.keyCommand.addShortcut(
|
||
'Del|Backspace',
|
||
this.removeLine.bind(this)
|
||
)
|
||
// 注册添加连接线的命令
|
||
this.mindMap.command.add('ADD_ASSOCIATIVE_LINE', this.addLine.bind(this))
|
||
// 监听鼠标移动事件
|
||
this.mindMap.on('mousemove', this.onMousemove.bind(this))
|
||
// 节点拖拽事件
|
||
this.mindMap.on('node_dragging', this.onNodeDragging.bind(this))
|
||
this.mindMap.on('node_dragend', this.onNodeDragend.bind(this))
|
||
// 拖拽控制点
|
||
window.addEventListener('mousemove', e => {
|
||
this.onControlPointMousemove(e)
|
||
})
|
||
window.addEventListener('mouseup', e => {
|
||
this.onControlPointMouseup(e)
|
||
})
|
||
}
|
||
|
||
// 创建箭头
|
||
createMarker() {
|
||
return this.draw.marker(20, 20, add => {
|
||
add.ref(2, 5)
|
||
add.size(10, 10)
|
||
add.attr('orient', 'auto-start-reverse')
|
||
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
|
||
})
|
||
}
|
||
|
||
// 渲染所有连线
|
||
renderAllLines() {
|
||
// 先移除
|
||
this.removeAllLines()
|
||
this.removeControls()
|
||
this.clearActiveLine()
|
||
let tree = this.mindMap.renderer.root
|
||
if (!tree) return
|
||
let idToNode = new Map()
|
||
let nodeToIds = new Map()
|
||
walk(
|
||
tree,
|
||
null,
|
||
cur => {
|
||
if (!cur) return
|
||
let data = cur.nodeData.data
|
||
if (
|
||
data.associativeLineTargets &&
|
||
data.associativeLineTargets.length > 0
|
||
) {
|
||
nodeToIds.set(cur, data.associativeLineTargets)
|
||
}
|
||
if (data.id) {
|
||
idToNode.set(data.id, cur)
|
||
}
|
||
},
|
||
() => {},
|
||
true,
|
||
0
|
||
)
|
||
nodeToIds.forEach((ids, node) => {
|
||
ids.forEach(id => {
|
||
let toNode = idToNode.get(id)
|
||
if (!node || !toNode) return
|
||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||
this.drawLine(startPoint, endPoint, node, toNode)
|
||
})
|
||
})
|
||
}
|
||
|
||
// 绘制连接线
|
||
drawLine(startPoint, endPoint, node, toNode) {
|
||
let {
|
||
associativeLineWidth,
|
||
associativeLineColor,
|
||
associativeLineActiveWidth,
|
||
associativeLineActiveColor
|
||
} = this.mindMap.themeConfig
|
||
// 箭头
|
||
this.markerPath
|
||
.stroke({ color: associativeLineColor })
|
||
.fill({ color: associativeLineColor })
|
||
// 路径
|
||
let { path: pathStr, controlPoints } = getNodeLinePath(
|
||
startPoint,
|
||
endPoint,
|
||
node,
|
||
toNode
|
||
)
|
||
// 虚线
|
||
let path = this.draw.path()
|
||
path
|
||
.stroke({
|
||
width: associativeLineWidth,
|
||
color: associativeLineColor,
|
||
dasharray: [6, 4]
|
||
})
|
||
.fill({ color: 'none' })
|
||
path.plot(pathStr)
|
||
path.marker('end', this.marker)
|
||
// 不可见的点击线
|
||
let clickPath = this.draw.path()
|
||
clickPath
|
||
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
|
||
.fill({ color: 'none' })
|
||
clickPath.plot(pathStr)
|
||
// 点击事件
|
||
clickPath.click(e => {
|
||
e.stopPropagation()
|
||
// 如果当前存在激活节点,那么取消激活节点
|
||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||
this.clearActiveNodes()
|
||
} else {
|
||
// 否则清除当前的关联线的激活状态,如果有的话
|
||
this.clearActiveLine()
|
||
// 保存当前激活的关联线信息
|
||
this.activeLine = [path, clickPath, node, toNode]
|
||
// 让不可见的点击线显示
|
||
clickPath.stroke({ color: associativeLineActiveColor })
|
||
// 渲染控制点和连线
|
||
this.renderControls(
|
||
startPoint,
|
||
endPoint,
|
||
controlPoints[0],
|
||
controlPoints[1]
|
||
)
|
||
this.mindMap.emit(
|
||
'associative_line_click',
|
||
path,
|
||
clickPath,
|
||
node,
|
||
toNode
|
||
)
|
||
}
|
||
})
|
||
this.lineList.push([path, clickPath, node, toNode])
|
||
}
|
||
|
||
// 移除所有连接线
|
||
removeAllLines() {
|
||
this.lineList.forEach(line => {
|
||
line[0].remove()
|
||
line[1].remove()
|
||
})
|
||
this.lineList = []
|
||
}
|
||
|
||
// 从当前激活节点开始创建连接线
|
||
createLineFromActiveNode() {
|
||
if (this.mindMap.renderer.activeNodeList.length <= 0) return
|
||
let node = this.mindMap.renderer.activeNodeList[0]
|
||
this.createLine(node)
|
||
}
|
||
|
||
// 创建连接线
|
||
createLine(fromNode) {
|
||
let { associativeLineWidth, associativeLineColor } =
|
||
this.mindMap.themeConfig
|
||
if (this.isCreatingLine || !fromNode) return
|
||
this.isCreatingLine = true
|
||
this.creatingStartNode = fromNode
|
||
this.creatingLine = this.draw.path()
|
||
this.creatingLine
|
||
.stroke({
|
||
width: associativeLineWidth,
|
||
color: associativeLineColor,
|
||
dasharray: [6, 4]
|
||
})
|
||
.fill({ color: 'none' })
|
||
this.creatingLine.marker('end', this.marker)
|
||
}
|
||
|
||
// 鼠标移动事件
|
||
onMousemove(e) {
|
||
if (!this.isCreatingLine) return
|
||
this.updateCreatingLine(e)
|
||
}
|
||
|
||
// 更新创建过程中的连接线
|
||
updateCreatingLine(e) {
|
||
let { x, y } = this.getTransformedEventPos(e)
|
||
let startPoint = getNodePoint(this.creatingStartNode)
|
||
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x, y)
|
||
this.creatingLine.plot(pathStr)
|
||
this.checkOverlapNode(x, y)
|
||
}
|
||
|
||
// 获取转换后的鼠标事件对象的坐标
|
||
getTransformedEventPos(e) {
|
||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||
let { scaleX, scaleY, translateX, translateY } =
|
||
this.mindMap.draw.transform()
|
||
return {
|
||
x: (x - translateX) / scaleX,
|
||
y: (y - translateY) / scaleY
|
||
}
|
||
}
|
||
|
||
// 检测当前移动到的目标节点
|
||
checkOverlapNode(x, y) {
|
||
this.overlapNode = null
|
||
bfsWalk(this.mindMap.renderer.root, node => {
|
||
if (node.nodeData.data.isActive) {
|
||
this.mindMap.renderer.setNodeActive(node, false)
|
||
}
|
||
if (node === this.creatingStartNode || this.overlapNode) {
|
||
return
|
||
}
|
||
let { left, top, width, height } = node
|
||
let right = left + width
|
||
let bottom = top + height
|
||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||
this.overlapNode = node
|
||
}
|
||
})
|
||
if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
|
||
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
|
||
}
|
||
}
|
||
|
||
// 完成创建连接线
|
||
completeCreateLine(node) {
|
||
if (this.creatingStartNode === node) return
|
||
this.addLine(this.creatingStartNode, node)
|
||
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
|
||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||
}
|
||
this.isCreatingLine = false
|
||
this.creatingStartNode = null
|
||
this.creatingLine.remove()
|
||
this.creatingLine = null
|
||
this.overlapNode = null
|
||
}
|
||
|
||
// 添加连接线
|
||
addLine(fromNode, toNode) {
|
||
if (!fromNode || !toNode) return
|
||
// 目标节点如果没有id,则生成一个id
|
||
let id = toNode.nodeData.data.id
|
||
if (!id) {
|
||
id = uuid()
|
||
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
|
||
id
|
||
})
|
||
}
|
||
// 将目标节点id保存起来
|
||
let list = fromNode.nodeData.data.associativeLineTargets || []
|
||
list.push(id)
|
||
// 保存控制点
|
||
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
|
||
let controlPoints = computeCubicBezierPathPoints(
|
||
startPoint.x,
|
||
startPoint.y,
|
||
endPoint.x,
|
||
endPoint.y
|
||
)
|
||
let offsetList =
|
||
fromNode.nodeData.data.associativeLineTargetControlOffsets || []
|
||
// 保存的实际是控制点和端点的差值,否则当节点位置改变了,控制点还是原来的位置,连线就不对了
|
||
offsetList[list.length - 1] = [
|
||
{
|
||
x: controlPoints[0].x - startPoint.x,
|
||
y: controlPoints[0].y - startPoint.y
|
||
},
|
||
{
|
||
x: controlPoints[1].x - endPoint.x,
|
||
y: controlPoints[1].y - endPoint.y
|
||
}
|
||
]
|
||
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
|
||
associativeLineTargets: list,
|
||
associativeLineTargetControlOffsets: offsetList
|
||
})
|
||
}
|
||
|
||
// 删除连接线
|
||
removeLine() {
|
||
if (!this.activeLine) return
|
||
let [, , node, toNode] = this.activeLine
|
||
this.removeControls()
|
||
let { associativeLineTargets, associativeLineTargetControlOffsets } =
|
||
node.nodeData.data
|
||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||
associativeLineTargets: associativeLineTargets.filter((_, index) => {
|
||
return index !== targetIndex
|
||
}),
|
||
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
|
||
? associativeLineTargetControlOffsets.filter((_, index) => {
|
||
return index !== targetIndex
|
||
})
|
||
: []
|
||
})
|
||
}
|
||
|
||
// 清除当前激活的节点
|
||
clearActiveNodes() {
|
||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||
}
|
||
}
|
||
|
||
// 清除激活的线
|
||
clearActiveLine() {
|
||
if (this.activeLine) {
|
||
this.activeLine[1].stroke({
|
||
color: 'transparent'
|
||
})
|
||
this.activeLine = null
|
||
this.removeControls()
|
||
}
|
||
}
|
||
|
||
// 处理节点正在拖拽事件
|
||
onNodeDragging() {
|
||
if (this.isNodeDragging) return
|
||
this.isNodeDragging = true
|
||
this.lineList.forEach(line => {
|
||
line[0].hide()
|
||
line[1].hide()
|
||
})
|
||
this.hideControls()
|
||
}
|
||
|
||
// 处理节点拖拽完成事件
|
||
onNodeDragend() {
|
||
if (!this.isNodeDragging) return
|
||
this.lineList.forEach(line => {
|
||
line[0].show()
|
||
line[1].show()
|
||
})
|
||
this.showControls()
|
||
this.isNodeDragging = false
|
||
}
|
||
|
||
// 创建控制点、连线节点
|
||
createControlNodes() {
|
||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||
// 连线
|
||
this.controlLine1 = this.draw
|
||
.line()
|
||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||
this.controlLine2 = this.draw
|
||
.line()
|
||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||
// 控制点
|
||
this.controlPoint1 = this.createOneControlNode('controlPoint1')
|
||
this.controlPoint2 = this.createOneControlNode('controlPoint2')
|
||
}
|
||
|
||
// 创建控制点
|
||
createOneControlNode(pointKey) {
|
||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||
return this.draw
|
||
.circle(this.controlPointDiameter)
|
||
.stroke({ color: associativeLineActiveColor })
|
||
.fill({ color: '#fff' })
|
||
.click(e => {
|
||
e.stopPropagation()
|
||
})
|
||
.mousedown(e => {
|
||
this.onControlPointMousedown(e, pointKey)
|
||
})
|
||
}
|
||
|
||
// 控制点的鼠标按下事件
|
||
onControlPointMousedown(e, pointKey) {
|
||
e.stopPropagation()
|
||
this.isControlPointMousedown = true
|
||
this.mousedownControlPointKey = pointKey
|
||
}
|
||
|
||
// 控制点的鼠标移动事件
|
||
onControlPointMousemove(e) {
|
||
if (
|
||
!this.isControlPointMousedown ||
|
||
!this.mousedownControlPointKey ||
|
||
!this[this.mousedownControlPointKey]
|
||
)
|
||
return
|
||
e.stopPropagation()
|
||
e.preventDefault()
|
||
let radius = this.controlPointDiameter / 2
|
||
// 转换鼠标当前的位置
|
||
let { x, y } = this.getTransformedEventPos(e)
|
||
this.controlPointMousemoveState.pos = {
|
||
x,
|
||
y
|
||
}
|
||
// 更新当前拖拽的控制点的位置
|
||
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
|
||
let [path, clickPath, node, toNode] = this.activeLine
|
||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||
this.controlPointMousemoveState.startPoint = startPoint
|
||
this.controlPointMousemoveState.endPoint = endPoint
|
||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||
this.controlPointMousemoveState.targetIndex = targetIndex
|
||
let offsets = []
|
||
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
|
||
if (!associativeLineTargetControlOffsets) {
|
||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
|
||
} else {
|
||
offsets = associativeLineTargetControlOffsets[targetIndex]
|
||
}
|
||
let point1 = null
|
||
let point2 = null
|
||
// 拖拽的是控制点1
|
||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||
point1 = {
|
||
x,
|
||
y
|
||
}
|
||
point2 = {
|
||
x: endPoint.x + offsets[1].x,
|
||
y: endPoint.y + offsets[1].y
|
||
}
|
||
// 更新控制点1的连线
|
||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||
} else {
|
||
// 拖拽的是控制点2
|
||
point1 = {
|
||
x: startPoint.x + offsets[0].x,
|
||
y: startPoint.y + offsets[0].y
|
||
}
|
||
point2 = {
|
||
x,
|
||
y
|
||
}
|
||
// 更新控制点2的连线
|
||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||
}
|
||
// 更新关联线
|
||
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
|
||
path.plot(pathStr)
|
||
clickPath.plot(pathStr)
|
||
}
|
||
|
||
// 控制点的鼠标移动事件
|
||
onControlPointMouseup(e) {
|
||
if (!this.isControlPointMousedown) return
|
||
e.stopPropagation()
|
||
e.preventDefault()
|
||
let { pos, startPoint, endPoint, targetIndex } =
|
||
this.controlPointMousemoveState
|
||
let [, , node] = this.activeLine
|
||
let offsetList = []
|
||
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
|
||
if (!associativeLineTargetControlOffsets) {
|
||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||
offsetList[targetIndex] = getDefaultControlPointOffsets(startPoint, endPoint)
|
||
} else {
|
||
offsetList = associativeLineTargetControlOffsets
|
||
}
|
||
let offset1 = null
|
||
let offset2 = null
|
||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||
// 更新控制点1数据
|
||
offset1 = {
|
||
x: pos.x - startPoint.x,
|
||
y: pos.y - startPoint.y
|
||
}
|
||
offset2 = offsetList[targetIndex][1]
|
||
} else {
|
||
// 更新控制点2数据
|
||
offset1 = offsetList[targetIndex][0]
|
||
offset2 = {
|
||
x: pos.x - endPoint.x,
|
||
y: pos.y - endPoint.y
|
||
}
|
||
}
|
||
offsetList[targetIndex] = [offset1, offset2]
|
||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||
associativeLineTargetControlOffsets: offsetList
|
||
})
|
||
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后
|
||
setTimeout(() => {
|
||
this.resetControlPoint()
|
||
}, 0)
|
||
}
|
||
|
||
// 复位控制点移动
|
||
resetControlPoint() {
|
||
this.isControlPointMousedown = false
|
||
this.mousedownControlPointKey = ''
|
||
this.controlPointMousemoveState = {
|
||
pos: null,
|
||
startPoint: null,
|
||
endPoint: null,
|
||
targetIndex: ''
|
||
}
|
||
}
|
||
|
||
// 渲染控制点
|
||
renderControls(startPoint, endPoint, point1, point2) {
|
||
if (!this.controlLine1) {
|
||
this.createControlNodes()
|
||
}
|
||
let radius = this.controlPointDiameter / 2
|
||
// 控制点和起终点的连线
|
||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||
// 控制点
|
||
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
|
||
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
|
||
}
|
||
|
||
// 删除控制点
|
||
removeControls() {
|
||
if (!this.controlLine1) return
|
||
;[
|
||
this.controlLine1,
|
||
this.controlLine2,
|
||
this.controlPoint1,
|
||
this.controlPoint2
|
||
].forEach(item => {
|
||
item.remove()
|
||
})
|
||
this.controlLine1 = null
|
||
this.controlLine2 = null
|
||
this.controlPoint1 = null
|
||
this.controlPoint2 = null
|
||
}
|
||
|
||
// 隐藏控制点
|
||
hideControls() {
|
||
if (!this.controlLine1) return
|
||
;[
|
||
this.controlLine1,
|
||
this.controlLine2,
|
||
this.controlPoint1,
|
||
this.controlPoint2
|
||
].forEach(item => {
|
||
item.hide()
|
||
})
|
||
}
|
||
|
||
// 显示控制点
|
||
showControls() {
|
||
if (!this.controlLine1) return
|
||
;[
|
||
this.controlLine1,
|
||
this.controlLine2,
|
||
this.controlPoint1,
|
||
this.controlPoint2
|
||
].forEach(item => {
|
||
item.show()
|
||
})
|
||
}
|
||
}
|
||
|
||
AssociativeLine.instanceName = 'associativeLine'
|
||
|
||
export default AssociativeLine
|