Feat: 实现关联线端点位置随鼠标拖拽变化

This commit is contained in:
eseasky 2023-08-29 20:09:57 +08:00 committed by yhyf123/chent
parent a31e93bacf
commit 3d86650f22
4 changed files with 280 additions and 53 deletions

View File

@ -99,12 +99,32 @@ class AssociativeLine {
// 创建箭头
createMarker() {
return this.draw.marker(20, 20, add => {
add.ref(2, 5)
add.ref(12, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
})
}
// 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标
updateAllLinesPos(node, toNode, associativeLineTargets) {
let startPoint = {}
let endPoint = {}
let nodeRange = 0
let nodeDir = 'right'
let toNodeRange = 0
let toNodeDir = 'right'
if (associativeLineTargets.startPoint && associativeLineTargets.endPoint) {
nodeRange = associativeLineTargets.startPoint.range || 0
nodeDir = associativeLineTargets.startPoint.dir || 'right'
startPoint = getNodePoint(node, nodeDir, nodeRange)
toNodeRange = associativeLineTargets.endPoint.range || 0
toNodeDir = associativeLineTargets.endPoint.dir || 'right'
endPoint = getNodePoint(toNode, toNodeDir, toNodeRange)
} else {
;[startPoint, endPoint] = computeNodePoints(node, toNode)
}
return [startPoint, endPoint]
}
// 渲染所有连线
renderAllLines() {
@ -137,10 +157,17 @@ class AssociativeLine {
0
)
nodeToIds.forEach((ids, node) => {
ids.forEach(id => {
let toNode = idToNode.get(id)
ids.forEach((id, index) => {
let toNode = idToNode.get(id.id)
if (!node || !toNode) return
let [startPoint, endPoint] = computeNodePoints(node, toNode)
const associativeLineTargets =
node.nodeData.data.associativeLineTargets[index] || {}
// 切换结构和布局,都会更新坐标
const [startPoint, endPoint] = this.updateAllLinesPos(
node,
toNode,
associativeLineTargets
)
this.drawLine(startPoint, endPoint, node, toNode)
})
})
@ -183,11 +210,28 @@ class AssociativeLine {
.fill({ color: 'none' })
clickPath.plot(pathStr)
// 文字
let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints })
let text = this.createText({
path,
clickPath,
node,
toNode,
startPoint,
endPoint,
controlPoints
})
// 点击事件
clickPath.click(e => {
e.stopPropagation()
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
this.setActiveLine({
path,
clickPath,
text,
node,
toNode,
startPoint,
endPoint,
controlPoints
})
})
// 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text)
@ -195,10 +239,17 @@ class AssociativeLine {
}
// 激活某根关联线
setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) {
let {
associativeLineActiveColor
} = this.mindMap.themeConfig
setActiveLine({
path,
clickPath,
text,
node,
toNode,
startPoint,
endPoint,
controlPoints
}) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 如果当前存在激活节点,那么取消激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.clearActiveNodes()
@ -243,8 +294,10 @@ class AssociativeLine {
// 创建连接线
createLine(fromNode) {
let { associativeLineWidth, associativeLineColor } =
this.mindMap.themeConfig
let {
associativeLineWidth,
associativeLineColor
} = this.mindMap.themeConfig
if (this.isCreatingLine || !fromNode) return
this.isCreatingLine = true
this.creatingStartNode = fromNode
@ -279,14 +332,33 @@ class AssociativeLine {
// 获取转换后的鼠标事件对象的坐标
getTransformedEventPos(e) {
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
let { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
let {
scaleX,
scaleY,
translateX,
translateY
} = this.mindMap.draw.transform()
return {
x: (x - translateX) / scaleX,
y: (y - translateY) / scaleY
}
}
// 计算节点偏移位置
getNodePos(node) {
const { translateX, translateY } = this.mindMap.draw.transform()
const { left, top, width, height } = node
let translateLeft = left + translateX
let translateTop = top + translateY
return {
left,
top,
translateLeft,
translateTop,
width,
height
}
}
// 检测当前移动到的目标节点
checkOverlapNode(x, y) {
this.overlapNode = null
@ -336,7 +408,12 @@ class AssociativeLine {
}
// 将目标节点id保存起来
let list = fromNode.nodeData.data.associativeLineTargets || []
list.push(id)
// 连线节点是否存在相同的id,存在则阻止添加关联线
const sameLine = list.some(item => item.id === id)
if (sameLine) {
return
}
list.push({ id })
// 保存控制点
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
let controlPoints = computeCubicBezierPathPoints(
@ -369,13 +446,16 @@ class AssociativeLine {
if (!this.activeLine) return
let [, , , node, toNode] = this.activeLine
this.removeControls()
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
node.nodeData.data
let {
associativeLineTargets,
associativeLineTargetControlOffsets,
associativeLineText
} = node.nodeData.data
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 更新关联线文本数据
let newAssociativeLineText = {}
if (associativeLineText) {
Object.keys(associativeLineText).forEach((item) => {
Object.keys(associativeLineText).forEach(item => {
if (item !== toNode.nodeData.data.id) {
newAssociativeLineText[item] = associativeLineText[item]
}

View File

@ -1,7 +1,7 @@
import {
getAssociativeLineTargetIndex,
joinCubicBezierPath,
computeNodePoints,
getNodePoint,
getDefaultControlPointOffsets
} from './associativeLineUtils'
@ -62,14 +62,22 @@ function onControlPointMousemove(e) {
// 更新当前拖拽的控制点的位置
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
let [path, clickPath, text, node, toNode] = this.activeLine
let [startPoint, endPoint] = computeNodePoints(node, toNode)
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
const {
associativeLineTargets,
associativeLineTargetControlOffsets
} = node.nodeData.data
const nodePos = this.getNodePos(node)
const toNodePos = this.getNodePos(toNode)
let [startPoint, endPoint] = this.updateAllLinesPos(
node,
toNode,
associativeLineTargets[targetIndex]
)
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)
@ -80,6 +88,7 @@ function onControlPointMousemove(e) {
let point2 = null
// 拖拽的是控制点1
if (this.mousedownControlPointKey === 'controlPoint1') {
startPoint = getNodePoint(nodePos, '', 0, e)
point1 = {
x,
y
@ -88,10 +97,22 @@ function onControlPointMousemove(e) {
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
if (startPoint) {
// 保存更新后的坐标
associativeLineTargets[targetIndex].startPoint = startPoint
this.controlPointMousemoveState.startPoint = startPoint
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
// 更新关联线
const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
this.updateTextPos(path, text)
this.updateTextEditBoxPos(text)
}
} else {
// 拖拽的是控制点2
endPoint = getNodePoint(toNodePos, '', 0, e)
point1 = {
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
@ -100,15 +121,20 @@ function onControlPointMousemove(e) {
x,
y
}
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
if (endPoint) {
// 保存更新后结束节点的坐标
associativeLineTargets[targetIndex].endPoint = endPoint
this.controlPointMousemoveState.endPoint = endPoint
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
// 更新关联线
const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
this.updateTextPos(path, text)
this.updateTextEditBoxPos(text)
}
}
// 更新关联线
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
this.updateTextPos(path, text)
this.updateTextEditBoxPos(text)
}
// 控制点的鼠标移动事件
@ -116,12 +142,18 @@ function onControlPointMouseup(e) {
if (!this.isControlPointMousedown) return
e.stopPropagation()
e.preventDefault()
let { pos, startPoint, endPoint, targetIndex } =
this.controlPointMousemoveState
let {
pos,
startPoint,
endPoint,
targetIndex
} = this.controlPointMousemoveState
let [, , , node] = this.activeLine
let offsetList = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
const {
associativeLineTargets,
associativeLineTargetControlOffsets
} = node.nodeData.data
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsetList[targetIndex] = getDefaultControlPointOffsets(
@ -150,7 +182,8 @@ function onControlPointMouseup(e) {
}
offsetList[targetIndex] = [offset1, offset2]
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineTargetControlOffsets: offsetList
associativeLineTargetControlOffsets: offsetList,
associativeLineTargets
})
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {

View File

@ -36,7 +36,7 @@ function showEditTextBox(g) {
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
@ -62,7 +62,8 @@ function showEditTextBox(g) {
).split(/\n/gim)
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px'
this.textEditNode.style.lineHeight = textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.lineHeight =
textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block'
@ -78,10 +79,12 @@ function onScale() {
// 更新文本编辑框位置
function updateTextEditBoxPos(g) {
let rect = g.node.getBoundingClientRect()
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
if (this.textEditNode) {
this.textEditNode.style.minWidth = `${rect.width + 10}px`
this.textEditNode.style.minHeight = `${rect.height + 6}px`
this.textEditNode.style.left = `${rect.left}px`
this.textEditNode.style.top = `${rect.top}px`
}
}
// 隐藏文本编辑框
@ -116,8 +119,10 @@ function getText(node, toNode) {
// 渲染关联线文字
function renderText(str, path, text) {
if (!str) return
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig
let {
associativeLineTextFontSize,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
text.clear()
let textArr = str.split(/\n/gim)
textArr.forEach((item, index) => {

View File

@ -1,7 +1,7 @@
// 获取目标节点在起始节点的目标数组中的索引
export const getAssociativeLineTargetIndex = (node, toNode) => {
return node.nodeData.data.associativeLineTargets.findIndex(item => {
return item === toNode.nodeData.data.id
return item.id === toNode.nodeData.data.id
})
}
@ -54,28 +54,137 @@ export const cubicBezierPath = (x1, y1, x2, y2) => {
)
}
// 计算关联线起始|结束坐标
export const calcPoint = (node, e) => {
const { left, top, translateLeft, translateTop, width, height } = node
const clientX = e.clientX
const clientY = e.clientY
// 中心点的坐标
const centerX = translateLeft + width / 2
const centerY = translateTop + height / 2
const translateCenterX = left + width / 2
const translateCenterY = top + height / 2
const theta = Math.atan(height / width)
// 矩形左上角坐标
const deltaX = clientX - centerX
const deltaY = centerY - clientY
// 方向值
const direction = Math.atan2(deltaY, deltaX)
// 默认坐标
let x = left + width
let y = top + height
if (direction < theta && direction >= -theta) {
// 右边
// 正切值 = 对边/邻边,对边 = 正切值*邻边
const range = direction * (width / 2)
if (direction < theta && direction >= 0) {
// 中心点上边
y = translateCenterY - range
} else if (direction >= -theta && direction < 0) {
// 中心点下方
y = translateCenterY - range
}
return {
x,
y,
dir: 'right',
range
}
} else if (direction >= theta && direction < Math.PI - theta) {
// 上边
y = top
let range = 0
if (direction < Math.PI / 2 - theta && direction >= theta) {
// 正切值 = 对边/邻边,邻边 = 对边/正切值
const side = height / 2 / direction
range = -side
// 中心点右侧
x = translateCenterX + side
} else if (
direction >= Math.PI / 2 - theta &&
direction < Math.PI - theta
) {
// 中心点左侧
const tanValue = (centerX - clientX) / (centerY - clientY)
const side = (height / 2) * tanValue
range = side
x = translateCenterX - side
}
return {
x,
y,
dir: 'top',
range
}
} else if (direction < -theta && direction >= theta - Math.PI) {
// 下边
let range = 0
if (direction >= theta - Math.PI / 2 && direction < -theta) {
// 中心点右侧
// 正切值 = 对边/邻边,邻边 = 对边/正切值
const side = height / 2 / direction
range = side
x = translateCenterX - side
} else if (
direction < theta - Math.PI / 2 &&
direction >= theta - Math.PI
) {
// 中心点左侧
const tanValue = (centerX - clientX) / (centerY - clientY)
const side = (height / 2) * tanValue
range = -side
x = translateCenterX + side
}
return {
x,
y,
dir: 'bottom',
range
}
}
// 左边
x = left
const tanValue = (centerY - clientY) / (centerX - clientX)
const range = tanValue * (width / 2)
if (direction >= -Math.PI && direction < theta - Math.PI) {
// 中心点右侧
y = translateCenterY - range
} else if (direction < Math.PI && direction >= Math.PI - theta) {
// 中心点左侧
y = translateCenterY - range
}
return {
x,
y,
dir: 'left',
range
}
}
// 获取节点的连接点
export const getNodePoint = (node, dir = 'right') => {
export const getNodePoint = (node, dir = 'right', range = 0, e = null) => {
let { left, top, width, height } = node
if (e) {
return calcPoint(node, e)
}
switch (dir) {
case 'left':
return {
x: left,
y: top + height / 2
y: top + height / 2 - range
}
case 'right':
return {
x: left + width,
y: top + height / 2
y: top + height / 2 - range
}
case 'top':
return {
x: left + width / 2,
x: left + width / 2 - range,
y: top
}
case 'bottom':
return {
x: left + width / 2,
x: left + width / 2 - range,
y: top + height
}
default:
@ -179,4 +288,4 @@ export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
y: controlPoints[1].y - endPoint.y
}
]
}
}