前面,本示例实现了折线连接线,简述了实现的思路和原理,也已知了一些缺陷。本章将处理一些缺陷的同时,实现支持连接点的自定义,一个节点可以定义多个连接点,最终可以满足类似图元接线的效果。

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

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

github源码

gitee源码

示例地址

一些调整

  • 把示例素材从 src 转移至 public 目录,拖入画布的素材改为异步加载
  • 移除部分示例素材
  • 一些开发过程中的测试用例可以在线加载

此前有些朋友说导入、导出有异常,估计是线上版本和线下版本的构建示例素材的文件 hash 后缀不一样,跨环境导入、导出无法加载图片导致的。现在调整后就应该正常了。

自定义连接点

先说明一下定义:

  1. // src/Render/types.ts
  2. export interface AssetInfoPoint {
  3. x: number
  4. y: number
  5. direction?: 'top' | 'bottom' | 'left' | 'right' // 人为定义连接点属于元素的什么方向
  6. }
  7. export interface AssetInfo {
  8. url: string
  9. points?: Array<AssetInfoPoint>
  10. }
  1. // src/Render/draws/LinkDraw.ts
  2. // 连接点
  3. export interface LinkDrawPoint {
  4. id: string
  5. groupId: string
  6. visible: boolean
  7. pairs: LinkDrawPair[]
  8. x: number
  9. y: number
  10. direction?: 'top' | 'bottom' | 'left' | 'right' // 人为定义连接点属于元素的什么方向
  11. }

一个素材除了原来的 url 信息外,增加了一个 points 的连接点数组,每个 point 除了记录了它的相对于素材的位置 x、y,还有方向的定义,目的是说明该连接点出入口方向,例如:

做这个定义的原因是,连接方向不可以预知,是与图元的含义有关。

不设定 direction 的话,就代表连接线可以从上下左右4个方向进出,如:

最佳实践应该另外实现一个连接点定义工具(也许后面有机会实现一个),多多支持~

  1. // src/App.vue
  2. // 从 public 加载静态资源 + 自定义连接点
  3. const assetsModules: Array<Types.AssetInfo> = [
  4. { "url": "./img/svg/ARRESTER_1.svg", points: [{ x: 101, y: 1, direction: 'top' }, { x: 101, y: 199, direction: 'bottom' }] },
  5. { "url": "./img/svg/ARRESTER_2.svg", points: [{ x: 101, y: 1, direction: 'top' }, { x: 101, y: 199, direction: 'bottom' }] },
  6. { "url": "./img/svg/ARRESTER_2_1.svg", points: [{ x: 101, y: 1, direction: 'top' }, { x: 101, y: 199, direction: 'bottom' }] },
  7. { "url": "./img/svg/BREAKER_CLOSE.svg", points: [{ x: 100, y: 1, direction: 'top' }, { x: 100, y: 199, direction: 'bottom' }] },
  8. { "url": "./img/svg/BREAKER_OPEN.svg", points: [{ x: 100, y: 1, direction: 'top' }, { x: 100, y: 199, direction: 'bottom' }] },
  9. // 略
  10. ]

素材拖入之前,需要携带 points 信息:

  1. // src/App.vue
  2. function onDragstart(e: GlobalEventHandlersEventMap['dragstart'], item: Types.AssetInfo) {
  3. if (e.dataTransfer) {
  4. e.dataTransfer.setData('src', item.url)
  5. e.dataTransfer.setData('points', JSON.stringify(item.points)) // 传递连接点信息
  6. e.dataTransfer.setData('type', item.url.match(/([^./]+)\.([^./]+)$/)?.[2] ?? '')
  7. }
  8. }

拖入之后,需要解析 points 信息:

  1. // src/Render/handlers/DragOutsideHandlers.ts
  2. drop: (e: GlobalEventHandlersEventMap['drop']) => {
  3. const src = e.dataTransfer?.getData('src')
  4. // 接收连接点信息
  5. let morePoints: Types.AssetInfoPoint[] = []
  6. const morePointsTxt = e.dataTransfer?.getData('points') ?? '[]'
  7. try {
  8. morePoints = JSON.parse(morePointsTxt)
  9. } catch (e) {
  10. console.error(e)
  11. }
  12. // 略
  13. // 默认连接点
  14. let points: Types.AssetInfoPoint[] = [
  15. // 左
  16. { x: 0, y: group.height() / 2, direction: 'left' },
  17. // 右
  18. {
  19. x: group.width(),
  20. y: group.height() / 2,
  21. direction: 'right'
  22. },
  23. // 上
  24. { x: group.width() / 2, y: 0, direction: 'top' },
  25. // 下
  26. {
  27. x: group.width() / 2,
  28. y: group.height(),
  29. direction: 'bottom'
  30. }
  31. ]
  32. // 自定义连接点 覆盖 默认连接点
  33. if (Array.isArray(morePoints) && morePoints.length > 0) {
  34. points = morePoints
  35. }
  36. // 连接点信息
  37. group.setAttrs({
  38. points: points.map(
  39. (o) =>
  40. ({
  41. ...o,
  42. id: nanoid(),
  43. groupId: group.id(),
  44. visible: false,
  45. pairs: [],
  46. direction: o.direction // 补充信息
  47. }) as LinkDrawPoint
  48. )
  49. })
  50. // 连接点(锚点)
  51. for (const point of group.getAttr('points') ?? []) {
  52. group.add(
  53. new Konva.Circle({
  54. name: 'link-anchor',
  55. id: point.id,
  56. x: point.x,
  57. y: point.y,
  58. radius: this.render.toStageValue(1),
  59. stroke: 'rgba(0,0,255,1)',
  60. strokeWidth: this.render.toStageValue(2),
  61. visible: false,
  62. direction: point.direction // 补充信息
  63. })
  64. )
  65. }
  66. // 略
  67. }

如果没有自定义连接点,这里会给予之前一样的 4 个默认连接点。

出入口修改

原来的逻辑就不能用了,需要重写一个。目标是计算出:沿着当前连接点的方向 与 不可通过区域其中一边的相交点,上图:

关注的就是这个绿色点(出入口):

就算这个点,用的是三角函数:

这里边长称为 offset,角度为 rotate,计算大概如下:

  1. const offset = gap * Math.atan(((90 - rotate) * Math.PI) / 180)

不同角度范围,计算略有不同,是根据多次测试得出的,有兴趣的朋友可以在优化精简一下。

完整方法有点长,四个角直接赋值,其余按不同角度范围计算:

  1. // 连接出入口(原来第二个参数是 最小区域,先改为 不可通过区域)
  2. getEntry(anchor: Konva.Node, groupForbiddenArea: Area, gap: number): Konva.Vector2d {
  3. // stage 状态
  4. const stageState = this.render.getStageState()
  5. const fromPos = anchor.absolutePosition()
  6. // 默认为 起点/终点 位置(无 direction 时的值)
  7. let x = fromPos.x - stageState.x,
  8. y = fromPos.y - stageState.y
  9. const direction = anchor.attrs.direction
  10. // 定义了 direction 的时候
  11. if (direction) {
  12. // 取整 连接点 锚点 旋转角度(保留 1 位小数点)
  13. const rotate = Math.round(anchor.getAbsoluteRotation() * 10) / 10
  14. // 利用三角函数,计算按 direction 方向与 不可通过区域 的相交点位置(即出/入口 entry)
  15. if (rotate === -45) {
  16. if (direction === 'top') {
  17. x = groupForbiddenArea.x1
  18. y = groupForbiddenArea.y1
  19. } else if (direction === 'bottom') {
  20. x = groupForbiddenArea.x2
  21. y = groupForbiddenArea.y2
  22. } else if (direction === 'left') {
  23. x = groupForbiddenArea.x1
  24. y = groupForbiddenArea.y2
  25. } else if (direction === 'right') {
  26. x = groupForbiddenArea.x2
  27. y = groupForbiddenArea.y1
  28. }
  29. } else if (rotate === 45) {
  30. if (direction === 'top') {
  31. x = groupForbiddenArea.x2
  32. y = groupForbiddenArea.y1
  33. } else if (direction === 'bottom') {
  34. x = groupForbiddenArea.x1
  35. y = groupForbiddenArea.y2
  36. } else if (direction === 'left') {
  37. x = groupForbiddenArea.x1
  38. y = groupForbiddenArea.y1
  39. } else if (direction === 'right') {
  40. x = groupForbiddenArea.x2
  41. y = groupForbiddenArea.y2
  42. }
  43. } else if (rotate === 135) {
  44. if (direction === 'top') {
  45. x = groupForbiddenArea.x2
  46. y = groupForbiddenArea.y2
  47. } else if (direction === 'bottom') {
  48. x = groupForbiddenArea.x1
  49. y = groupForbiddenArea.y1
  50. } else if (direction === 'left') {
  51. x = groupForbiddenArea.x2
  52. y = groupForbiddenArea.y1
  53. } else if (direction === 'right') {
  54. x = groupForbiddenArea.x1
  55. y = groupForbiddenArea.y2
  56. }
  57. } else if (rotate === -135) {
  58. if (direction === 'top') {
  59. x = groupForbiddenArea.x1
  60. y = groupForbiddenArea.y2
  61. } else if (direction === 'bottom') {
  62. x = groupForbiddenArea.x2
  63. y = groupForbiddenArea.y1
  64. } else if (direction === 'left') {
  65. x = groupForbiddenArea.x2
  66. y = groupForbiddenArea.y2
  67. } else if (direction === 'right') {
  68. x = groupForbiddenArea.x1
  69. y = groupForbiddenArea.y1
  70. }
  71. } else if (rotate > -45 && rotate < 45) {
  72. const offset = gap * Math.tan((rotate * Math.PI) / 180)
  73. if (direction === 'top') {
  74. x = fromPos.x - stageState.x + offset
  75. y = groupForbiddenArea.y1
  76. } else if (direction === 'bottom') {
  77. x = fromPos.x - stageState.x - offset
  78. y = groupForbiddenArea.y2
  79. } else if (direction === 'left') {
  80. x = groupForbiddenArea.x1
  81. y = fromPos.y - stageState.y - offset
  82. } else if (direction === 'right') {
  83. x = groupForbiddenArea.x2
  84. y = fromPos.y - stageState.y + offset
  85. }
  86. } else if (rotate > 45 && rotate < 135) {
  87. const offset = gap * Math.atan(((90 - rotate) * Math.PI) / 180)
  88. if (direction === 'top') {
  89. x = groupForbiddenArea.x2
  90. y = fromPos.y - stageState.y - offset
  91. } else if (direction === 'bottom') {
  92. x = groupForbiddenArea.x1
  93. y = fromPos.y - stageState.y + offset
  94. } else if (direction === 'left') {
  95. x = fromPos.x - stageState.x - offset
  96. y = groupForbiddenArea.y1
  97. } else if (direction === 'right') {
  98. x = fromPos.x - stageState.x + offset
  99. y = groupForbiddenArea.y2
  100. }
  101. } else if ((rotate > 135 && rotate <= 180) || (rotate >= -180 && rotate < -135)) {
  102. const offset = gap * Math.tan((rotate * Math.PI) / 180)
  103. if (direction === 'top') {
  104. x = fromPos.x - stageState.x - offset
  105. y = groupForbiddenArea.y2
  106. } else if (direction === 'bottom') {
  107. x = fromPos.x - stageState.x + offset
  108. y = groupForbiddenArea.y1
  109. } else if (direction === 'left') {
  110. x = groupForbiddenArea.x2
  111. y = fromPos.y - stageState.y + offset
  112. } else if (direction === 'right') {
  113. x = groupForbiddenArea.x1
  114. y = fromPos.y - stageState.y - offset
  115. }
  116. } else if (rotate > -135 && rotate < -45) {
  117. const offset = gap * Math.atan(((90 + rotate) * Math.PI) / 180)
  118. if (direction === 'top') {
  119. x = groupForbiddenArea.x1
  120. y = fromPos.y - stageState.y - offset
  121. } else if (direction === 'bottom') {
  122. x = groupForbiddenArea.x2
  123. y = fromPos.y - stageState.y + offset
  124. } else if (direction === 'left') {
  125. x = fromPos.x - stageState.x - offset
  126. y = groupForbiddenArea.y2
  127. } else if (direction === 'right') {
  128. x = fromPos.x - stageState.x + offset
  129. y = groupForbiddenArea.y1
  130. }
  131. }
  132. }
  133. return { x, y } as Konva.Vector2d
  134. }

原来的算法起点、终点 与 连接点一一对应,科室现在新的计算方法得出的出入口x、y坐标与连接点不再总是存在同一方向一致(因为被旋转),所以现在把算法的起点、终点改为出入口对应:

  1. // 出口、入口 -> 算法 起点、终点
  2. if (columns[x] === fromEntry.x && rows[y] === fromEntry.y) {
  3. matrix[y][x] = 1
  4. matrixStart = { x, y }
  5. }
  6. if (columns[x] === toEntry.x && rows[y] === toEntry.y) {
  7. matrix[y][x] = 1
  8. matrixEnd = { x, y }
  9. }

上面提到没有定义 direction 的连接点可以从不同方向出入,所以会进行下面处理:

  1. // 没有定义方向(给于十字可通过区域)
  2. // 如,从:
  3. // 1 1 1
  4. // 1 0 1
  5. // 1 1 1
  6. // 变成:
  7. // 1 0 1
  8. // 0 0 0
  9. // 1 0 1
  10. if (!fromAnchor.attrs.direction) {
  11. if (columns[x] === fromEntry.x || rows[y] === fromEntry.y) {
  12. if (
  13. x >= columnFromStart &&
  14. x <= columnFromEnd &&
  15. y >= rowFromStart &&
  16. y <= rowFromEnd
  17. ) {
  18. matrix[y][x] = 1
  19. }
  20. }
  21. }
  22. if (!toAnchor.attrs.direction) {
  23. if (columns[x] === toEntry.x || rows[y] === toEntry.y) {
  24. if (x >= columnToStart && x <= columnToEnd && y >= rowToStart && y <= rowToEnd) {
  25. matrix[y][x] = 1
  26. }
  27. }
  28. }

最后在绘制连线的时候,补上连接点(起点、终点)即可:

  1. this.group.add(
  2. new Konva.Line({
  3. name: 'link-line',
  4. // 用于删除连接线
  5. groupId: fromGroup.id(),
  6. pointId: fromPoint.id,
  7. pairId: pair.id,
  8. //
  9. points: _.flatten([
  10. [
  11. this.render.toStageValue(fromAnchorPos.x),
  12. this.render.toStageValue(fromAnchorPos.y)
  13. ], // 补充 起点
  14. ...way.map((o) => [
  15. this.render.toStageValue(columns[o.x]),
  16. this.render.toStageValue(rows[o.y])
  17. ]),
  18. [this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)] // 补充 终点
  19. ]),
  20. stroke: 'red',
  21. strokeWidth: 2
  22. })
  23. )

测试一下

已知缺陷

从 Issue 中得知,当节点进行说 transform rotate 旋转的时候,对齐就会出问题。大家多多支持,后面抽空研究处理一下(-_-)。。。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

前端使用 Konva 实现可视化设计器(15)- 自定义连接点、连接优化的更多相关文章

  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. Nacos2.0的K8s服务发现生态应用及规划

    ​简介:Nacos 是阿里巴巴于 2018 年开源的注册中心及配置中心产品,帮助用户的分布式微服务应用进行服务发现和配置管理功能.随着 Nacos2.0 版本的发布,在性能和扩展性上取得较大突破后,社 ...

  2. 喜马拉雅 Apache RocketMQ 消息治理实践

    ​简介:本文通过喜马拉雅的RocketMQ治理实践分享,让大家了解使用消息中间件过程中可能遇到的问题,避免实战中踩坑. 作者:曹融,来自喜马拉雅,从事微服务和消息相关中间件开发. ​ 本文通过喜马拉雅 ...

  3. 快手基于 Flink 构建实时数仓场景化实践

    简介: 一文了解快手基于 Flink 构建的实时数仓架构,以及一些难题的解决方案. 本文整理自快手数据技术专家李天朔在 5 月 22 日北京站 Flink Meetup 分享的议题<快手基于 F ...

  4. Win32 使用 CreateProcess 方法让任务管理器里的命令行不显示应用文件路径

    本文记录一个 Win32 的有趣行为,调用 CreateProcess 方法传入特别的参数,可以让任务管理器里的命令行不显示应用文件路径 开始之前,先看看下面这张有趣的图片 可以看到我编写的 Svca ...

  5. Redis 5集群部署

    1.redis特点 (1)基于内存 (2)可持久化数据 (3)具有丰富的数据结构类型,适应非关系型数据的存储需求 (4)支持绝大多数主流开发语言,如C.C++.Java.Python.R.JavaSc ...

  6. DNS(3) -- dns常用命令-rndc-dig-host-nslookup

    目录 1 bind自带客户端命令 1.1 rndc命令 1.2 检查配置文件语法 2 客户端测试命令 2.1 dig命令 2.2 host命令 2.3 nslookup命令 1 bind自带客户端命令 ...

  7. make编译报错:fatal error: filesystem: 没有那个文件或目录 #include <filesystem>

    报错: fatal error: filesystem: 没有那个文件或目录 #include(filesystem) 解决方法一: 修改头文件 #include <experimental/f ...

  8. WPF新建viewModel实例化成员的注意事项

    不要用表达式体去初始化一个用做数据源(比如ItemSource)的引用类型成员.比如这种 public List<MainWindowItem> Items => new List& ...

  9. T2T-ViT:更多的局部结构信息,更高效的主干网络 | ICCV 2021

    论文提出了T2T-ViT模型,引入tokens-to-token(T2T)模块有效地融合图像的结构信息,同时借鉴CNN结果设计了deep-narrow的ViT主干网络,增强特征的丰富性.在ImageN ...

  10. c++ RTTI Runtime Type Identification 运行阶段类型识别

    NoVirtualBase* NvirBase = new NovirtualDerivd(); NvirBase->print(); // auto nd1 = dynamic_cast< ...