前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

news/2025/1/19 11:24:22/文章来源:https://www.cnblogs.com/xachary/p/18337764

本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

模式切换

image

前置工作

连接线 模式种类

// 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 // 记录 连接线 类型                         }                       ]                     }                     // 略                   }                 }               }             }             // 略           }         })         // 略       }     }   }}
}

直线

image

绘制直线相对简单,通过判断 连接对(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 {         // 略,原算法画连接线逻辑       }   }}
}

折线

image

绘制折线,先人为定义 3 种“点”: 1、连接点,就是原来就有的。 2、拐点(待拐),蓝色的,从未拖动过的,一旦拖动,会新增拐点记录。 3、拐点(已拐),绿色的,已经拖动过的,依然可以拖动,但不会新增拐点记录。

image

请留意下方代码的注释,关键:

  • 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源码

示例地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/776384.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024牛客多校第5场

很神奇的场hh,大家一起坐牢,多好啊! B 找规律,这种题一般都是多模拟几个数据然后猜出来#include<bits/stdc++.h> using namespace std;inline int read() {int x=0;bool f=1;char ch=getchar();for(;ch<0||ch>9;ch=getchar())f^=(ch==-);for(;ch>=0&&am…

15. 序列化模块json和pickle、os模块

1. 序列化模块1.1 序列化与反序列化 (1)序列化 将原本的python数据类型字典、列表、元组 转换成json格式字符串的过程就叫序列化 (2)反序列化 将json格式字符串转换成python数据类型字典、列表、元组的过程就叫反序列化 (3)为什么要序列化 计算机文件中没有字典这种数据类…

AI表情神同步!LivePortrait安装配置,一键包,使用教程

快手在AI视频这领域还真有点东西,视频生成工具“可灵”让大家玩得不亦乐乎。现在又开源了一款超好玩的表情同步(表情控制)项目。 一看这图片,就知道是小视频平台出的,充满了娱乐性。发布没几天就已经有8000+Star。项目简介 LivePortrait 是一款由快手团队开发的高效肖像动…

右下角wifi图案点击无可用wifi/更新网卡驱动时遇到错误代码56的解决办法

1.问题 如下图所示,我这里遇到明明有wifi,但是无法检索到任何有用wifi的情况。2.解决方法 参考:电脑WIFI消失,网卡驱动Intel(R) Wi-Fi 6 AX201 160MHz感叹号报错 解决方案集合 —— 无线WI-FI功能缺失,Intel(R) Wi-Fi 6 AX201 160MHz异常,驱动更新错误 2.1 问题原因 当时…

单例模式及其思想

本文包括以下几点↓结论:设计模式不是简单地将一个固定的代码框架套用到项目中,而是一种严谨的编程思想,旨在提供解决特定问题的经验和指导。 单例模式(Singleton Pattern) 意图 旨在确保类只有一个实例,并提供一个全局访问点以访问该实例。 适用性 当你希望系统中只有一…

数组part02

2024年8月1日,今天学习了数组的第二部分。 1.巩固了昨天的双指针问题,即滑动窗口/双指针;注意,双指针是为了减少for循环,使用的时候小心循环的写法和快慢指针的增长方法。 2.学习了数组模拟的螺旋矩阵问题,注意循环不变量; 3.学习了前缀和的方法,前缀和常用来解决区间和…

windwos文件句柄数限制

1、修改注册表,位置如下: HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Windows2、设置 1、GDIProcessHandleQuota此项设置GDI句柄数量,默认值为2710(16进制)/10000(10进制),该值的允许范围为 256 ~ 16384 ,将其调整为大于默认的10000的值。如果您的…

P5665 [CSP-S2019] 划分

讲解 P5665 [CSP-S2019] 划分。由朴素 dp 入手,先用二分优化,然后用走指针优化,之后注意到单调性,将状态数压缩,然后使用单调队列优化转移。思路: 首先求出 \(a\) 的前缀和数组 \(s\)。 考虑动态规划,令 \(dp_{i,j}\) 表示以 \(i\) 结尾,末尾有 \(j\) 个为一组的最小答…

「代码随想录算法训练营」第二十六天 | 贪心算法 part4

452. 用最少数量的箭引爆气球题目链接:https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/ 题目难度:中等 文章讲解:https://programmercarl.com/0452.用最少数量的箭引爆气球.html 视频讲解:https://www.bilibili.com/video/BV1SA41167xe 题目状态…

dbnet crnn java中文ocr识别

Table of ContentsAbout Getting Started Result ContactAbout完整项目:https://github.com/jiangnanboy/dbnet_crnn_java本项目利用java,javacv,onnx以及djl矩阵计算等技术加载文本检测模型dbnet与文本识别模型crnn,完成ocr的识别推理。 包含模型的完整项目请从右侧releases…