Files
mind-map/simple-mind-map/src/layouts/Base.js
2025-04-07 16:59:37 +08:00

681 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import MindMapNode from '../core/render/node/MindMapNode'
import { CONSTANTS, initRootNodePositionMap } from '../constants/constant'
import Lru from '../utils/Lru'
import { createUid } from '../utils/index'
// 布局基类
class Base {
// 构造函数
constructor(renderer) {
// 渲染实例
this.renderer = renderer
// 控制实例
this.mindMap = renderer.mindMap
// 绘图对象
this.draw = this.mindMap.draw
this.lineDraw = this.mindMap.lineDraw
// 根节点
this.root = null
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
// 当initRootNodePosition不为默认的值时根节点的位置距默认的配置时根节点距离的差值
this.rootNodeCenterOffset = null
}
// 计算节点位置
doLayout() {
throw new Error('【computed】方法为必要方法需要子类进行重写')
}
// 连线
renderLine() {
throw new Error('【renderLine】方法为必要方法需要子类进行重写')
}
// 定位展开收缩按钮
renderExpandBtn() {
throw new Error('【renderExpandBtn】方法为必要方法需要子类进行重写')
}
// 概要节点
renderGeneralization() {}
// 通过uid缓存节点
cacheNode(uid, node) {
// 记录本次渲染时的节点
this.renderer.nodeCache[uid] = node
// 缓存所有渲染过的节点
this.lru.add(uid, node)
}
// 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() {
return this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_THEME)
}
// 层级类型改变
checkIsLayerTypeChange(oldIndex, newIndex) {
if (oldIndex >= 2 && newIndex >= 2) return false
if (oldIndex >= 2 && newIndex < 2) return true
if (oldIndex < 2 && newIndex >= 2) return true
}
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
if (this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_LAYOUT)) {
node.needRerenderExpandBtnPlaceholderRect = true
}
}
// 节点节点数据是否发生了改变
checkIsNodeDataChange(lastData, curData) {
if (lastData) {
// 对比忽略激活状态和展开收起状态
lastData = typeof lastData === 'string' ? JSON.parse(lastData) : lastData
lastData.isActive = curData.isActive
lastData.expand = curData.expand
lastData = JSON.stringify(lastData)
} else {
// 只在都有数据时才进行对比
return false
}
return lastData !== JSON.stringify(curData)
}
// 检查库前置或后置内容是否改变了
checkNodeFixChange(newNode, nodeInnerPrefixData, nodeInnerPostfixData) {
// 库前置内容是否改变了
let isNodeInnerPrefixChange = false
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (item.updateNodeData) {
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
if (isChange) {
isNodeInnerPrefixChange = isChange
}
}
})
// 库后置内容是否改变了
let isNodeInnerPostfixChange = false
this.mindMap.nodeInnerPostfixList.forEach(item => {
if (item.updateNodeData) {
const isChange = item.updateNodeData(newNode, nodeInnerPostfixData)
if (isChange) {
isNodeInnerPostfixChange = isChange
}
}
})
return isNodeInnerPrefixChange || isNodeInnerPostfixChange
}
// 创建节点实例
createNode(data, parent, isRoot, layerIndex, index, ancestors) {
// 创建节点
// 库前置内容数据
const nodeInnerPrefixData = {}
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (item.createNodeData) {
const [key, value] = item.createNodeData({
data,
parent,
ancestors,
layerIndex,
index
})
nodeInnerPrefixData[key] = value
}
})
// 库后置内容数据
const nodeInnerPostfixData = {}
this.mindMap.nodeInnerPostfixList.forEach(item => {
if (item.createNodeData) {
const [key, value] = item.createNodeData({
data,
parent,
ancestors,
layerIndex,
index
})
nodeInnerPostfixData[key] = value
}
})
const uid = data.data.uid
let newNode = null
// 数据上保存了节点引用,那么直接复用节点
if (data && data._node && !this.renderer.reRender) {
newNode = data._node
// 节点层级改变了
const isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
)
newNode.reset()
newNode.layerIndex = layerIndex
if (isRoot) {
newNode.isRoot = true
} else {
newNode.parent = parent._node
}
this.cacheNode(data._node.uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
// 库前置或后置内容是否改变了
const isNodeInnerFixChange = this.checkNodeFixChange(
newNode,
nodeInnerPrefixData,
nodeInnerPostfixData
)
// 主题或主题配置改变了
const isResizeSource = this.checkIsNeedResizeSources()
// 节点数据改变了
const isNodeDataChange = this.checkIsNodeDataChange(
data._node.nodeDataSnapshot,
data.data
)
// 重新计算节点大小和布局
if (
isResizeSource ||
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
newNode.getData('needUpdate') ||
isNodeInnerFixChange
) {
newNode.getSize()
newNode.needLayout = true
}
this.checkGetGeneralizationChange(newNode, isResizeSource)
} else if (
(this.lru.has(uid) || this.renderer.lastNodeCache[uid]) &&
!this.renderer.reRender
) {
// 节点数据上没有节点实例
// 但是通过uid在节点缓存池中找到了缓存的节点
// 或者在上一次渲染缓存对象中找到了节点
// 也可以直接复用
newNode = this.lru.get(uid) || this.renderer.lastNodeCache[uid]
// 保存该节点上一次的数据
const lastData = JSON.stringify(newNode.getData())
// 节点层级改变了
const isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
)
newNode.reset()
newNode.nodeData = newNode.handleData(data || {})
newNode.layerIndex = layerIndex
if (isRoot) {
newNode.isRoot = true
} else {
newNode.parent = parent._node
}
this.cacheNode(uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
data._node = newNode
// 主题或主题配置改变了需要重新计算节点大小和布局
const isResizeSource = this.checkIsNeedResizeSources()
// 点数据改变了
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
// 库前置或后置内容是否改变了
const isNodeInnerFixChange = this.checkNodeFixChange(
newNode,
nodeInnerPrefixData,
nodeInnerPostfixData
)
// 重新计算节点大小和布局
if (
isResizeSource ||
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
newNode.getData('needUpdate') ||
isNodeInnerFixChange
) {
newNode.getSize()
newNode.needLayout = true
}
this.checkGetGeneralizationChange(newNode, isResizeSource)
} else {
// 创建新节点
const newUid = uid || createUid()
newNode = new MindMapNode({
data,
uid: newUid,
renderer: this.renderer,
mindMap: this.mindMap,
draw: this.draw,
layerIndex,
isRoot,
parent: !isRoot ? parent._node : null,
...nodeInnerPrefixData
})
// uid保存到数据上为了节点复用
data.data.uid = newUid
this.cacheNode(newUid, newNode)
// 数据关联实际节点
data._node = newNode
}
// 如果该节点数据是已激活状态,那么添加到激活节点列表里
if (data.data.isActive) {
this.renderer.addNodeToActiveList(newNode)
}
// 如果当前节点在激活节点列表里,那么添加上激活的状态
if (this.mindMap.renderer.findActiveNodeIndex(newNode) !== -1) {
newNode.setData({
isActive: true
})
}
// 根节点
if (isRoot) {
this.root = newNode
} else {
// 互相收集
parent._node.addChildren(newNode)
}
return newNode
}
// 检查概要节点是否需要更新
checkGetGeneralizationChange(node, isResizeSource) {
const generalizationList = node.getData('generalization')
if (
generalizationList &&
node._generalizationList &&
node._generalizationList.length > 0
) {
node._generalizationList.forEach((item, index) => {
const gNode = item.generalizationNode
const oldData = gNode.getData()
const newData = generalizationList[index]
if (
isResizeSource ||
(newData && JSON.stringify(oldData) !== JSON.stringify(newData))
) {
if (newData) {
gNode.nodeData.data = newData
}
gNode.getSize()
gNode.needLayout = true
}
})
}
}
// 格式化节点位置
formatPosition(value, size, nodeSize) {
if (typeof value === 'number') {
return value
} else if (initRootNodePositionMap[value] !== undefined) {
return size * initRootNodePositionMap[value]
} else if (/^\d\d*%$/.test(value)) {
return (Number.parseFloat(value) / 100) * size
} else {
return (size - nodeSize) / 2
}
}
// 规范initRootNodePosition配置
formatInitRootNodePosition(pos) {
const { CENTER } = CONSTANTS.INIT_ROOT_NODE_POSITION
if (!pos || !Array.isArray(pos) || pos.length < 2) {
pos = [CENTER, CENTER]
}
return pos
}
// 定位节点到画布中间
setNodeCenter(node, position) {
let { initRootNodePosition } = this.mindMap.opt
initRootNodePosition = this.formatInitRootNodePosition(
position || initRootNodePosition
)
node.left = this.formatPosition(
initRootNodePosition[0],
this.mindMap.width,
node.width
)
node.top = this.formatPosition(
initRootNodePosition[1],
this.mindMap.height,
node.height
)
}
// 当initRootNodePosition配置不为默认的['center','center']时,计算当前配置和默认配置情况下,根节点位置的差值
getRootCenterOffset(width, height) {
// 因为根节点的大小不会影响这个差值,所以计算一次就足够了
if (this.rootNodeCenterOffset) return this.rootNodeCenterOffset
let { initRootNodePosition } = this.mindMap.opt
const { CENTER } = CONSTANTS.INIT_ROOT_NODE_POSITION
initRootNodePosition = this.formatInitRootNodePosition(initRootNodePosition)
if (
initRootNodePosition[0] === CENTER &&
initRootNodePosition[1] === CENTER
) {
// 如果initRootNodePosition是默认的那么不需要计算
this.rootNodeCenterOffset = {
x: 0,
y: 0
}
} else {
// 否则需要计算当前配置和默认配置的差值
const tmpNode = {
width: width,
height: height
}
const tmpNode2 = {
width: width,
height: height
}
this.setNodeCenter(tmpNode, [CENTER, CENTER])
this.setNodeCenter(tmpNode2)
this.rootNodeCenterOffset = {
x: tmpNode2.left - tmpNode.left,
y: tmpNode2.top - tmpNode.top
}
}
return this.rootNodeCenterOffset
}
// 更新子节点属性
updateChildren(children, prop, offset) {
children.forEach(item => {
item[prop] += offset
if (item.children && item.children.length && !item.hasCustomPosition()) {
// 适配自定义位置
this.updateChildren(item.children, prop, offset)
}
})
}
// 更新子节点多个属性
updateChildrenPro(children, props) {
children.forEach(item => {
Object.keys(props).forEach(prop => {
item[prop] += props[prop]
})
if (item.children && item.children.length && !item.hasCustomPosition()) {
// 适配自定义位置
this.updateChildrenPro(item.children, props)
}
})
}
// 递归计算节点的宽度
getNodeAreaWidth(node, withGeneralization = false) {
let widthArr = []
let totalGeneralizationNodeWidth = 0
let loop = (node, width) => {
if (withGeneralization && node.checkHasGeneralization()) {
totalGeneralizationNodeWidth += node._generalizationNodeWidth
}
if (node.children.length) {
width += node.width / 2
node.children.forEach(item => {
loop(item, width)
})
} else {
width += node.width
widthArr.push(width)
}
}
loop(node, 0)
return Math.max(...widthArr) + totalGeneralizationNodeWidth
}
// 二次贝塞尔曲线
quadraticCurvePath(x1, y1, x2, y2, v = false) {
let cx, cy
if (v) {
cx = x1 + (x2 - x1) * 0.8
cy = y1 + (y2 - y1) * 0.2
} else {
cx = x1 + (x2 - x1) * 0.2
cy = y1 + (y2 - y1) * 0.8
}
return `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
}
// 三次贝塞尔曲线
cubicBezierPath(x1, y1, x2, y2, v = false) {
let cx1, cy1, cx2, cy2
if (v) {
cx1 = x1
cy1 = y1 + (y2 - y1) / 2
cx2 = x2
cy2 = cy1
} else {
cx1 = x1 + (x2 - x1) / 2
cy1 = y1
cx2 = cx1
cy2 = y2
}
return `M ${x1},${y1} C ${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`
}
// 根据a,b两个点的位置计算去除圆角大小后的新的b点
computeNewPoint(a, b, radius = 0) {
// x坐标相同
if (a[0] === b[0]) {
// b在a下方
if (b[1] > a[1]) {
return [b[0], b[1] - radius]
} else {
// b在a上方
return [b[0], b[1] + radius]
}
} else if (a[1] === b[1]) {
// y坐标相同
// b在a右边
if (b[0] > a[0]) {
return [b[0] - radius, b[1]]
} else {
return [b[0] + radius, b[1]]
}
}
}
// 创建一段折线路径
// 最后一个拐角支持圆角
createFoldLine(list) {
const { lineRadius } = this.mindMap.themeConfig
const len = list.length
let path = ''
let radiusPath = ''
if (len >= 3 && lineRadius > 0) {
const start = list[len - 3]
const center = list[len - 2]
const end = list[len - 1]
// 如果三点在一条直线,那么不用处理
const isOneLine =
(start[0].toFixed(0) === center[0].toFixed(0) &&
center[0].toFixed(0) === end[0].toFixed(0)) ||
(start[1].toFixed(0) === center[1].toFixed(0) &&
center[1].toFixed(0) === end[1].toFixed(0))
if (!isOneLine) {
const cStart = this.computeNewPoint(start, center, lineRadius)
const cEnd = this.computeNewPoint(end, center, lineRadius)
radiusPath = `Q ${center[0]},${center[1]} ${cEnd[0]},${cEnd[1]}`
list.splice(len - 2, 1, cStart, radiusPath)
}
}
list.forEach((item, index) => {
if (typeof item === 'string') {
path += item
} else {
const [x, y] = item
if (index === 0) {
path += `M ${x},${y}`
} else {
path += `L ${x},${y}`
}
}
})
return path
}
// 获取节点的marginX
getMarginX(layerIndex) {
const { themeConfig, opt } = this.mindMap
const { second, node } = themeConfig
const hoverRectPadding = opt.hoverRectPadding * 2
return layerIndex === 1
? second.marginX + hoverRectPadding
: node.marginX + hoverRectPadding
}
// 获取节点的marginY
getMarginY(layerIndex) {
const { themeConfig, opt } = this.mindMap
const { second, node } = themeConfig
const hoverRectPadding = opt.hoverRectPadding * 2
return layerIndex === 1
? second.marginY + hoverRectPadding
: node.marginY + hoverRectPadding
}
// 获取节点包括概要在内的宽度
getNodeWidthWithGeneralization(node) {
return Math.max(
node.width,
node.checkHasGeneralization() ? node._generalizationNodeWidth : 0
)
}
// 获取节点包括概要在内的高度
getNodeHeightWithGeneralization(node) {
return Math.max(
node.height,
node.checkHasGeneralization() ? node._generalizationNodeHeight : 0
)
}
// 获取节点的边界值
/**
* dir生长方向h水平、v垂直
* isLeft是否向左生长
*/
getNodeBoundaries(node, dir) {
let { generalizationLineMargin, generalizationNodeMargin } =
this.mindMap.themeConfig
let walk = root => {
let _left = Infinity
let _right = -Infinity
let _top = Infinity
let _bottom = -Infinity
if (root.children && root.children.length > 0) {
root.children.forEach(child => {
let { left, right, top, bottom } = walk(child)
// 概要内容的宽度
let generalizationWidth =
child.checkHasGeneralization() && child.getData('expand')
? child._generalizationNodeWidth + generalizationNodeMargin
: 0
// 概要内容的高度
let generalizationHeight =
child.checkHasGeneralization() && child.getData('expand')
? child._generalizationNodeHeight + generalizationNodeMargin
: 0
if (left - (dir === 'h' ? generalizationWidth : 0) < _left) {
_left = left - (dir === 'h' ? generalizationWidth : 0)
}
if (right + (dir === 'h' ? generalizationWidth : 0) > _right) {
_right = right + (dir === 'h' ? generalizationWidth : 0)
}
if (top < _top) {
_top = top
}
if (bottom + (dir === 'v' ? generalizationHeight : 0) > _bottom) {
_bottom = bottom + (dir === 'v' ? generalizationHeight : 0)
}
})
}
let cur = {
left: root.left,
right: root.left + root.width,
top: root.top,
bottom: root.top + root.height
}
return {
left: cur.left < _left ? cur.left : _left,
right: cur.right > _right ? cur.right : _right,
top: cur.top < _top ? cur.top : _top,
bottom: cur.bottom > _bottom ? cur.bottom : _bottom
}
}
let { left, right, top, bottom } = walk(node)
return {
left,
right,
top,
bottom,
generalizationLineMargin,
generalizationNodeMargin
}
}
// 获取指定索引区间的子节点的边界范围
getChildrenBoundaries(node, dir, startIndex = 0, endIndex) {
let { generalizationLineMargin, generalizationNodeMargin } =
this.mindMap.themeConfig
const children = node.children.slice(startIndex, endIndex + 1)
let left = Infinity
let right = -Infinity
let top = Infinity
let bottom = -Infinity
children.forEach(item => {
const cur = this.getNodeBoundaries(item, dir)
left = cur.left < left ? cur.left : left
right = cur.right > right ? cur.right : right
top = cur.top < top ? cur.top : top
bottom = cur.bottom > bottom ? cur.bottom : bottom
})
return {
left,
right,
top,
bottom,
generalizationLineMargin,
generalizationNodeMargin
}
}
// 获取节点概要的渲染边界
getNodeGeneralizationRenderBoundaries(item, dir) {
let res = null
// 区间
if (item.range) {
res = this.getChildrenBoundaries(
item.node,
dir,
item.range[0],
item.range[1]
)
} else {
// 整体概要
res = this.getNodeBoundaries(item.node, dir)
}
return res
}
// 获取节点实际存在几个子节点
getNodeActChildrenLength(node) {
return node.nodeData.children && node.nodeData.children.length
}
// 设置连线样式
setLineStyle(style, line, path, childNode) {
line.plot(this.transformPath(path))
style && style(line, childNode, true)
}
// 转换路径,可以转换成特殊风格的线条样式
transformPath(path) {
const { customTransformNodeLinePath } = this.mindMap.opt
if (customTransformNodeLinePath) {
return customTransformNodeLinePath(path)
} else {
return path
}
}
}
export default Base