ETJava Beta | Java    注册   登录
  • 搜索:
  • 前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

    发表于      阅读(1)     博客类别:Crawler     转自: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源码

    示例地址