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

请大家动动小手,给我一个免费的 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源码

示例地址

前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线的更多相关文章

  1. 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!

    官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ...

  2. (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用

    一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ...

  3. Windows Phone 十二、设计器同步

    在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ...

  4. WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?

    目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ...

  5. ActiveReports 9 新功能:可视化查询设计器(VQD)介绍

    在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ...

  6. VS2015 android 设计器不能可视化问题解决。

    近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ...

  7. 可视化流程设计——流程设计器演示(基于Silverlight)

    上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...

  8. F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)

          Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ...

  9. 纯JS Web在线可拖拽的流程设计器

    F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ...

  10. Type Script 在流程设计器的落地实践

    流程设计器项目介绍 从事过BPM行业的大佬必然对流程建模工具非常熟悉,做为WFMC三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...

随机推荐

  1. 【论文笔记】R-CNN系列之论文理解

    [深度学习]总目录 RCNN全称region with CNN features,即用CNN提取出Region Proposals中的featues.RCNN系列论文(R-CNN,Fast R-CNN ...

  2. 搭建单机版伪分布式Hadoop+Scala+spark

    搭建单机版伪分布式Hadoop+Scala+spark 修改ip [root@master ~]# nmcli connection add ifname ens32 con-name ens32 a ...

  3. 利用QEMU模拟大端序机器

    简介 当前我们安装虚拟机,一般小端机器比较多,有时候想模拟大端机器测试程序,这时就有模拟大端机器的需求. 参考:利用 QEMU USER 模式运行 mips 程序 - sinpo828 - 博客园 ( ...

  4. 重写学习 localStorage 与 sessionStorage

    localStorage 与 sessionStorage localStorage 与 sessionStorage 很多小伙伴对它们俩都很熟悉了: 最熟悉的莫过下面这2条 1,localStora ...

  5. Linux扩展篇-shell编程(十)-shell范式

    shell编程提供一个范式,有利于统一程序风格,增加可读性. 范式: ASSIGN SHELL/指定壳 DESCRIPTION/程序说明 BODY/程序体 扩展: 1.ASSIGN SHELL/指定壳 ...

  6. Oracle使用序列和触发器设置自增字段

    一.创建一张工作表 例: create table tv(ID NUMBER primary key,TVNAME VARCHAR(16),ISPASS NUMBER);   二.先创建一个序列 cr ...

  7. Spring Data JPA 学习笔记1 - JPA与Spring Data

    标记[跳过]的未来完善 1 理解JPA 1.1 什么是持久化? 当一个软件关闭的时候,软件内储存的状态数据还能在下次开启时被恢复,这就是持久化.对象持久化是指每个独立的对象的生命周期都能不依赖应用程序 ...

  8. 增补博客 第六篇 python 电子算盘

    珠算测试器 题目描述]设计一个珠算测试器,要求能够完成珠算加减法的测试.具体的要求功能如下:(1)用户启动测试,输入用户名后系统随机生成特定数目的加减法测试题:(2) 要求测试使用表盘式或数字时秒表进 ...

  9. 前端学习之nvm

    接手了新的项目 需要使用nodejs,但是版本又不同如何解决呢 如果自己下载配置环境变量也太复杂了 下载nvm https://nvm.uihtm.com/download.html 使用nvm 下载 ...

  10. Fastjson基础环境配置与Java基础概念

    Preface 此篇系列文章将会从 Java 的基础语法开始,以 Fastjson 的各个反序列化漏洞分析为结尾,详细记录如何从一个具有基础面向对象编程但毫无 Java 基础的小白成长为了解 Fast ...