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

news2025/1/13 8:09:35

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

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

示例地址

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最全个人笔记【Makefile】

1. 基本概念 1.1 make是什么 当一个项目中要编译的文件很多时&#xff0c;手工使用编译器一个个进行编译&#xff0c;很明显不具有可操作性&#xff0c;此时必须借助某些软件&#xff0c;协助我们有序地、正确地自动编译整个工程的所有该编译的文件。这样的软件被称为 工程管…

Simulink|基于粒子群算法的永磁同步电机多参数辨识

目录 主要内容 模型研究 结果一览 下载链接 主要内容 仿真程序参考文献《改进粒子群算法的永磁同步电机多参数辨识》&#xff0c;采用粒子群算法与simulink模型结合的方式&#xff0c;对永磁同步电机进行多参数辨识。程序以定子绕组电阻、d轴电感、q轴电感和永磁…

ai写作免费版工具上哪找?一文详解5大ai写作神器

面对写作难题&#xff0c;你是否曾感到力不从心&#xff1f;从创意枯竭到语法错误&#xff0c;每个写作环节都可能成为挑战。但如今&#xff0c;有了ai写作工具的帮助&#xff0c;这些难题都能迎刃而解。今天&#xff0c;就让我们一起来看看ai写作免费网页版应该怎么选吧&#…

Win11系统文件资源管理器鼠标右键卡顿解决方法

引用链接&#xff1a; Windows 11文件资源管理器崩溃怎么解决&#xff1f;看看这7个解决办法&#xff01;

订单搜索分页查询业务

文章目录 概要整体架构流程技术细节小结 概要 订单搜索分页查询是电商、物流、零售等众多行业中的常见需求&#xff0c;主要用于管理和分析大量订单数据. 需求分析以及接口设计 技术细节 1.Controller层: 根据接口设计来写 ApiOperation("订单搜索")GetMapping(…

使用plink和git进行数据处理

首先使用git进行plink环境配置&#xff0c;显示环境安装成功&#xff0c;在此环境下可以使用plink 在基因型数据处理过程中&#xff0c;看到vcf文件后首要做的就是将vcf文件转成二进制文件&#xff0c;输入命令 plink --vcf genotype.vcf --allow-extra-chr --recode --out tes…

滑动窗口代码实现

public int minSubArrayLen(int target, int[] nums) {int len nums.length;int res len 1;//最大是len&#xff0c;如果最后res结果还是n1说明没有答案返回0int sum0;int left0;for(int right0; right<len; right){sumnums[right];while(sum>target){//不需要判断rig…

算法 —— 递推

目录 递推 数楼梯 斐波那契数列 一维数组递推 P1002 过河卒 二维数组递推 P1044 栈 卡特兰数 递推 将一个很大的任务分解成规模小一些的子任务&#xff0c;子任务分成更小的子任务&#xff0c;直到遇到初始条件&#xff0c;最后整理归纳解决大任务的思想就是递推与递…

Kafka基本概念,工作流程介绍

1、消息队列与Kafka 1.1、Kafka简介 Kafka使用scala开发&#xff0c;支持多语言客户端&#xff08;c、java、python、go等&#xff09; Kafka最先由LinkedIn公司开发&#xff0c;之后成为Apache的顶级项目。 Kafka是一个分布式的、分区化、可复制提交的日志服务 LinkedIn使…

麒麟系统查看和修改ip

查看ip ifconfig ifconfig enp0s3 192.168.1.110

ROS中自定义头文件和源文件

今天分享一下如何在ROS中进行头文件和源文件的调用。案例是实现输出"Hello World" 本文的头文件(即.h文件)放在include/${project_name}路径下, 源文件和可执行文件则在src文件夹下 第一步&#xff1a;创建工作空间&#xff0c;创建包&#xff0c;使用vscode打开工作…

mysql高级语句的查询语句

一、排序语法&#xff0c;关键字排序 升序和降序 默认的排序方式就是升序 升序&#xff1a;ASC 降序&#xff1a;DESC 配合语法&#xff1a;order by 语法 1、升序 select * from info order by name; 根据名字升序排序&#xff0c;不需要加ASC select * from info order…

kubernetes prometheus 系列| helm 部署prometheus+grafana

一、环境准备 部署k8s集群 k8sv1.26直通车搭建 安装存储类 nfs动态供给直通车 安装helm工具 https://github.com/helm/helm/releases tar -zxvf helm-v3.5.4-linux-amd64.tar.gz sudo mv linux-amd64/helm /usr/local/bin/helm chmod ox /usr/local/bin/helm helm version二、…

day05 1.多线程基本概念 2.线程支持函数(多线程编程)

1、使用两个线程完成两个文件的拷贝&#xff0c;分支线程1拷贝前一半&#xff0c;分支线程2拷贝后一半&#xff0c;主线程回收两个分支线程的资源 #include <myhead.h>int copy_file(const char *srcfile,const char *destfile,int start,int len);struct Buff {const c…

TMGM官网中国大陆地区客户

仅限TMGM官网中国大陆地区客户且每位客户每月仅限参与一次 申请限制&#xff1a;客户完成某一档位并点击兑换礼物后&#xff0c;该活动结束。若客户继续入金达到下一个档位&#xff0c;可以获得多份该档位礼物&#xff0c;此时无法通过活动板块继续申请&#xff0c;需要联系客…

案例 —— 怪物出水

一&#xff0c;Ocean Setup 设置海洋Surface Grid&#xff08;使用Large Ocean工具架&#xff09; 调节默认Grid的大小尺寸及细分&#xff08;使用非常小尺寸来测试&#xff09;&#xff1b;调整频谱输入点的多少&#xff0c;频谱Grid Size&#xff0c;波浪方向&#xff0c;速度…

Python基础教程(三)类和对象、异常处理和模块

8.类与对象 8.1 面向对象 面向对象的三大基本特征: 封装、继承、多态。 在面向对象编程中&#xff0c;封装&#xff08;Encapsulation&#xff09;是一种将数据和操作&#xff08;方法&#xff09;组合在一起的机制。通过封装&#xff0c;我们可以隐藏数据的具体实现细节&am…

鸿蒙系统开发【设备安全服务-应用设备状态检测】安全

设备安全服务-应用设备状态检测 介绍 本示例向您介绍如何在应用中获取DeviceToken用于对应用的设备状态进行检测。 需要使用设备安全服务接口 kit.DeviceSecurityKit。 效果预览 Sample工程的配置与使用 在DevEco中配置Sample工程的步骤如下 [创建项目]及[应用]。打开Sam…

算法训练1

01背包问题 背包状态方程----动态规划 二维dp 使用 f[i][j] max(f[i-1][j] ,f[i-1][j - w[i]] v[i]); 伪代码&#xff1a; int dp[100][100]; void test6() {int n; //装备数量int m; //背包容量int v[105], w[105]; //前面空间&#xff0c;后面价值for (int i 1; i &l…

快速排序(上)

快速排序 前言 快速排序算法是最流行的排序算法,且有充足的理由,因为在大多数情况下,快速排序都是最快的。所以学习快速排序算法十分有必要。当然&#xff0c;既然它这么好&#xff0c;也就不太容易理解。 正文 Hoare版快排 快速排序是Hoare在1962年提出的一种二叉树结构的…