本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
github源码
gitee源码
示例地址
模式切换
前置工作
连接线 模式种类
// src/Render/types.ts
export enum LinkType { 'auto' = 'auto', 'straight' = 'straight', // 直线 'manual' = 'manual' // 手动折线
}
连接线 模式状态
// src/Render/draws/LinkDraw.ts
// 连接线(临时)
export interface LinkDrawState { // 略 linkType: Types.LinkType // 连接线类型 linkManualing: boolean // 是否 正在操作拐点
}
连接线 模式切换方法
// src/Render/draws/LinkDraw.ts
/** * 修改当前连接线类型 * @param linkType Types.LinkType */ changeLinkType(linkType: Types.LinkType) { this.state.linkType = linkType this.render.config?.on?.linkTypeChange?.(this.state.linkType)}
连接线 模式切换按钮
<!-- src/App.vue -->
<button @click="onLinkTypeChange(Types.LinkType.auto)" :disabled="currentLinkType === Types.LinkType.auto">连接线:自动</button>
<button @click="onLinkTypeChange(Types.LinkType.straight)" :disabled="currentLinkType === Types.LinkType.straight">连接线:直线</button>
<button @click="onLinkTypeChange(Types.LinkType.manual)" :disabled="currentLinkType === Types.LinkType.manual">连接线:手动</button>
连接线 模式切换事件
// src/App.vue
const currentLinkType = ref(Types.LinkType.auto)
function onLinkTypeChange(linkType: Types.LinkType) {(render?.draws[Draws.LinkDraw.name] as Draws.LinkDraw).changeLinkType(linkType)
}
当前 连接对(pair) 记录当前 连接线 模式
// src/Render/draws/LinkDraw.ts
export class LinkDraw extends Types.BaseDraw implements Types.Draw { // 略 override draw() { // 略 // 连接点 for (const point of points) { // 略 // 非 选择中 if (group && !group.getAttr('selected')) { // 略 const anchor = this.render.layer.findOne(`#${point.id}`)
if (anchor) { // 略 circle.on('mouseup', () => { if (this.state.linkingLine) { // 略 // 不同连接点 if (line.circle.id() !== circle.id()) { // 略 if (toGroup) { // 略 if (fromPoint) { // 略 if (toPoint) { if (Array.isArray(fromPoint.pairs)) { fromPoint.pairs = [ ...fromPoint.pairs, { // 略 linkType: this.state.linkType // 记录 连接线 类型 } ] } // 略 } } } } // 略 } }) // 略 } } }}
}
直线
绘制直线相对简单,通过判断 连接对(pair)记录的 连接线 模式,从起点绘制一条 Line 到终点即可:
// src/Render/draws/LinkDraw.ts
export class LinkDraw extends Types.BaseDraw implements Types.Draw { // 略 override draw() { // 略 // 连接线 for (const pair of pairs) { if (pair.linkType === Types.LinkType.manual) { // 略,手动折线 } else if (pair.linkType === Types.LinkType.straight) { // 直线
if (fromGroup && toGroup && fromPoint && toPoint) { const fromAnchor = fromGroup.findOne(`#${fromPoint.id}`) const toAnchor = toGroup.findOne(`#${toPoint.id}`)
// 锚点信息 const fromAnchorPos = this.getAnchorPos(fromAnchor) const toAnchorPos = this.getAnchorPos(toAnchor)
const linkLine = new Konva.Line({ name: 'link-line', // 用于删除连接线 groupId: fromGroup.id(), pointId: fromPoint.id, pairId: pair.id, linkType: pair.linkType,
points: _.flatten([ [ this.render.toStageValue(fromAnchorPos.x), this.render.toStageValue(fromAnchorPos.y) ], [this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)] ]), stroke: 'red', strokeWidth: 2 })
this.group.add(linkLine) } } else { // 略,原算法画连接线逻辑 } }}
}
折线
绘制折线,先人为定义 3 种“点”: 1、连接点,就是原来就有的。 2、拐点(待拐),蓝色的,从未拖动过的,一旦拖动,会新增拐点记录。 3、拐点(已拐),绿色的,已经拖动过的,依然可以拖动,但不会新增拐点记录。
请留意下方代码的注释,关键:
- fromGroup 会记录 拐点 manualPoints。
- 连接线 的绘制是从 起点 -> 拐点(们)-> 终点(linkPoints)。
- 拐点正在拖动时,绘制临时的虚线 Line。
- 分别处理 拐点(待拐)和 拐点(已拐)两种情况。
处理 拐点(待拐)和 拐点(已拐)主要区别是:
- 处理 拐点(待拐),遍历 linkPoints 的时候,是成对遍历的。
- 处理 拐点(已拐),遍历 linkPoints 的时候,是跳过 起点 和 终点 的。
- 拖动 拐点(待拐),会新增拐点记录。
- 拖动 拐点(已拐),不会新增拐点记录。
// src/Render/draws/LinkDraw.ts
export class LinkDraw extends Types.BaseDraw implements Types.Draw { // 略 override draw() { // 略 // 连接线 for (const pair of pairs) { if (pair.linkType === Types.LinkType.manual) { // 手动折线
if (fromGroup && toGroup && fromPoint && toPoint) { const fromAnchor = fromGroup.findOne(`#${fromPoint.id}`) const toAnchor = toGroup.findOne(`#${toPoint.id}`)
// 锚点信息 const fromAnchorPos = this.getAnchorPos(fromAnchor) const toAnchorPos = this.getAnchorPos(toAnchor)
// 拐点(已拐)记录 const manualPoints: Array<{ x: number; y: number }> = Array.isArray( fromGroup.getAttr('manualPoints') ) ? fromGroup.getAttr('manualPoints') : []
// 连接点 + 拐点 const linkPoints = [ [ this.render.toStageValue(fromAnchorPos.x), this.render.toStageValue(fromAnchorPos.y) ], ...manualPoints.map((o) => [o.x, o.y]), [this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)] ]
// 连接线 const linkLine = new Konva.Line({ name: 'link-line', // 用于删除连接线 groupId: fromGroup.id(), pointId: fromPoint.id, pairId: pair.id, linkType: pair.linkType,
points: _.flatten(linkPoints), stroke: 'red', strokeWidth: 2 })
this.group.add(linkLine)
// 正在拖动效果 const manualingLine = new Konva.Line({ stroke: '#ff0000', strokeWidth: 2, points: [], dash: [4, 4] }) this.group.add(manualingLine)
// 拐点
// 拐点(待拐) for (let i = 0; i < linkPoints.length - 1; i++) { const circle = new Konva.Circle({ id: nanoid(), pairId: pair.id, x: (linkPoints[i][0] + linkPoints[i + 1][0]) / 2, y: (linkPoints[i][1] + linkPoints[i + 1][1]) / 2, radius: this.render.toStageValue(this.render.bgSize / 2), stroke: 'rgba(0,0,255,0.1)', strokeWidth: this.render.toStageValue(1), name: 'link-manual-point', // opacity: 0, linkManualIndex: i // 当前拐点位置 })
// hover 效果 circle.on('mouseenter', () => { circle.stroke('rgba(0,0,255,0.8)') document.body.style.cursor = 'pointer' }) circle.on('mouseleave', () => { if (!circle.attrs.dragStart) { circle.stroke('rgba(0,0,255,0.1)') document.body.style.cursor = 'default' } })
// 拐点操作 circle.on('mousedown', () => { const pos = circle.getAbsolutePosition()
// 记录操作开始状态 circle.setAttrs({ // 开始坐标 dragStartX: pos.x, dragStartY: pos.y, // 正在操作 dragStart: true })
// 标记状态 - 正在操作拐点 this.state.linkManualing = true }) this.render.stage.on('mousemove', () => { if (circle.attrs.dragStart) { // 正在操作 const pos = this.render.stage.getPointerPosition() if (pos) { // 磁贴 const { pos: transformerPos } = this.render.attractTool.attract({ x: pos.x, y: pos.y, width: 1, height: 1 })
// 移动拐点 circle.setAbsolutePosition(transformerPos)
// 正在拖动效果 const tempPoints = [...linkPoints] tempPoints.splice(circle.attrs.linkManualIndex + 1, 0, [ this.render.toStageValue(transformerPos.x - stageState.x), this.render.toStageValue(transformerPos.y - stageState.y) ]) manualingLine.points(_.flatten(tempPoints)) } } }) circle.on('mouseup', () => { const pos = circle.getAbsolutePosition()
if ( Math.abs(pos.x - circle.attrs.dragStartX) > this.option.size || Math.abs(pos.y - circle.attrs.dragStartY) > this.option.size ) { // 操作移动距离达到阈值
// stage 状态 const stageState = this.render.getStageState()
// 记录(插入)拐点 manualPoints.splice(circle.attrs.linkManualIndex, 0, { x: this.render.toStageValue(pos.x - stageState.x), y: this.render.toStageValue(pos.y - stageState.y) }) fromGroup.setAttr('manualPoints', manualPoints) }
// 操作结束 circle.setAttrs({ dragStart: false })
// state 操作结束 this.state.linkManualing = false
// 销毁 circle.destroy() manualingLine.destroy()
// 更新历史 this.render.updateHistory()
// 重绘 this.render.redraw() })
this.group.add(circle) }
// 拐点(已拐) for (let i = 1; i < linkPoints.length - 1; i++) { const circle = new Konva.Circle({ id: nanoid(), pairId: pair.id, x: linkPoints[i][0], y: linkPoints[i][1], radius: this.render.toStageValue(this.render.bgSize / 2), stroke: 'rgba(0,100,0,0.1)', strokeWidth: this.render.toStageValue(1), name: 'link-manual-point', // opacity: 0, linkManualIndex: i // 当前拐点位置 })
// hover 效果 circle.on('mouseenter', () => { circle.stroke('rgba(0,100,0,1)') document.body.style.cursor = 'pointer' }) circle.on('mouseleave', () => { if (!circle.attrs.dragStart) { circle.stroke('rgba(0,100,0,0.1)') document.body.style.cursor = 'default' } })
// 拐点操作 circle.on('mousedown', () => { const pos = circle.getAbsolutePosition()
// 记录操作开始状态 circle.setAttrs({ dragStartX: pos.x, dragStartY: pos.y, dragStart: true })
// 标记状态 - 正在操作拐点 this.state.linkManualing = true }) this.render.stage.on('mousemove', () => { if (circle.attrs.dragStart) { // 正在操作 const pos = this.render.stage.getPointerPosition() if (pos) { // 磁贴 const { pos: transformerPos } = this.render.attractTool.attract({ x: pos.x, y: pos.y, width: 1, height: 1 })
// 移动拐点 circle.setAbsolutePosition(transformerPos)
// 正在拖动效果 const tempPoints = [...linkPoints] tempPoints[circle.attrs.linkManualIndex] = [ this.render.toStageValue(transformerPos.x - stageState.x), this.render.toStageValue(transformerPos.y - stageState.y) ] manualingLine.points(_.flatten(tempPoints)) } } }) circle.on('mouseup', () => { const pos = circle.getAbsolutePosition()
if ( Math.abs(pos.x - circle.attrs.dragStartX) > this.option.size || Math.abs(pos.y - circle.attrs.dragStartY) > this.option.size ) { // 操作移动距离达到阈值
// stage 状态 const stageState = this.render.getStageState()
// 记录(更新)拐点 manualPoints[circle.attrs.linkManualIndex - 1] = { x: this.render.toStageValue(pos.x - stageState.x), y: this.render.toStageValue(pos.y - stageState.y) } fromGroup.setAttr('manualPoints', manualPoints) }
// 操作结束 circle.setAttrs({ dragStart: false })
// state 操作结束 this.state.linkManualing = false
// 销毁 circle.destroy() manualingLine.destroy()
// 更新历史 this.render.updateHistory()
// 重绘 this.render.redraw() })
this.group.add(circle) } } } else if (pair.linkType === Types.LinkType.straight) { // 略,直线 } else { // 略,原算法画连接线逻辑 } }}
}
最后,关于 linkManualing 状态,会用在 2 个地方,避免和其它交互产生冲突:
// src/Render/handlers/DragHandlers.ts// 略export class DragHandlers implements Types.Handler {// 略 handlers = {stage: {mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {// 拐点操作中,防止异常拖动if (!(this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state.linkManualing) {// 略}},// 略}}
}
// src/Render/tools/LinkTool.ts// 略
export class LinkTool {// 略pointsVisible(visible: boolean, group?: Konva.Group) {// 略// 拐点操作中,此处不重绘if (!(this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state.linkManualing) {// 重绘this.render.redraw()}}// 略
}
Done!
More Stars please!勾勾手指~
源码
gitee源码
示例地址