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

这一章实现的连接线,目前仅支持直线连接,为了能够不影响原有的其它功能,尝试了2、3个实现思路,最终实测这个实现方式目前来说最为合适了。

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

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

github源码

gitee源码

示例地址

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

相关定义

  • 连接点
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

记录了连接点相关信息,并不作为素材而存在,仅记录信息,即导出导入的时候,并不会出现所谓的连接点节点。

它存放在节点身上,因此导出、导入自然而然就可以持久化了。

src/Render/draws/LinkDraw.ts

// 连接点 export interface LinkDrawPoint {   id: string   groupId: string   visible: boolean   pairs: LinkDrawPair[]   x: number   y: number } 
  • 连接对
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

一个连接点,记录从该点出发的多条连接线信息,作为连接对信息存在。

src/Render/draws/LinkDraw.ts

// 连接对 export interface LinkDrawPair {   id: string   from: {     groupId: string     pointId: string   }   to: {     groupId: string     pointId: string   } } 
  • 连接点(锚点)
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

它是拖入素材的时候生成的真实节点,归属于所在的节点中,存在却不可见,关键作用是同步连接点真实坐标,尤其是节点发生 transform 时候,必须依赖它获得 transform 后连接点变化。

src/Render/handlers/DragOutsideHandlers.ts

// 略 		drop: (e: GlobalEventHandlersEventMap['drop']) => { // 略                const points = [                 // 左                 { x: 0, y: group.height() / 2 },                 // 右                 {                   x: group.width(),                   y: group.height() / 2                 },                 // 上                 { x: group.width() / 2, y: 0 },                 // 下                 {                   x: group.width() / 2,                   y: group.height()                 }               ]                // 连接点信息               group.setAttrs({                 points: points.map(                   (o) =>                     ({                       ...o,                       id: nanoid(),                       groupId: group.id(),                       visible: true,                       pairs: []                     }) as LinkDrawPoint                 )               })                // 连接点(锚点)               for (const point of group.getAttr('points') ?? []) {                 group.add(                   new Konva.Circle({                     name: 'link-anchor',                     id: point.id,                     x: point.x,                     y: point.y,                     radius: this.render.toStageValue(1),                     stroke: 'rgba(0,0,255,1)',                     strokeWidth: this.render.toStageValue(2),                     visible: false                   })                 )               }                group.on('mouseenter', () => {                 // 显示 连接点                 this.render.linkTool.pointsVisible(true, group)               })                // hover 框(多选时才显示)               group.add(                 new Konva.Rect({                   id: 'hoverRect',                   width: image.width(),                   height: image.height(),                   fill: 'rgba(0,255,0,0.3)',                   visible: false                 })               )                group.on('mouseleave', () => {                 // 隐藏 连接点                 this.render.linkTool.pointsVisible(false, group)                  // 隐藏 hover 框                 group.findOne('#hoverRect')?.visible(false)               })                // 略 } // 略 
  • 连接线
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

根据连接点信息,绘制的线条,也不作为素材而存在,导出导入的时候,也不会出现所谓的连接点节点。不过,在导出图片、SVG和用于预览框的时候,会直接利用线条节点导出、显示。

src/Render/tools/ImportExportTool.ts

// 略   /**    * 获得显示内容    * @param withLink 是否包含线条    * @returns     */   getView(withLink: boolean = false) {   	// 复制画布     const copy = this.render.stage.clone()     // 提取 main layer 备用     const main = copy.find('#main')[0] as Konva.Layer     const cover = copy.find('#cover')[0] as Konva.Layer     // 暂时清空所有 layer     copy.removeChildren()      // 提取节点     let nodes = main.getChildren((node) => {       return !this.render.ignore(node)     })      if (withLink) {       nodes = nodes.concat(         cover.getChildren((node) => {           return node.name() === Draws.LinkDraw.name         })       )     }        	// 略   } // 略 

src/Render/draws/PreviewDraw.ts

  override draw() {       // 略              const main = this.render.stage.find('#main')[0] as Konva.Layer       const cover = this.render.stage.find('#cover')[0] as Konva.Layer       // 提取节点       const nodes = [         ...main.getChildren((node) => {           return !this.render.ignore(node)         }),         // 补充连线         ...cover.getChildren((node) => {           return node.name() === Draws.LinkDraw.name         })       ]              // 略   } 
  • 连接线(临时)
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

起点鼠标按下 -> 拖动显示线条 -> 终点鼠标释放 -> 产生连接信息 LinkDrawPoint. LinkDrawPair

// 连接线(临时) export interface LinkDrawState {   linkingLine: {     group: Konva.Group     circle: Konva.Circle     line: Konva.Line   } | null } 

代码文件

新增几个关键的代码文件:

src/Render/draws/LinkDraw.ts

根据 连接点.链接对 绘制 连接点、连接线,及其相关的事件处理

它的绘制顺序,应该放在绘制 比例尺、预览框之前。

src/Render/handlers/LinkHandlers.ts

根据 连接线(临时)信息,绘制/移除 连接线(临时)

src/Render/tools/LinkTool.ts

移除连接线,控制 连接点 的显示/隐藏

移除连接线,实际上就是移除其 连接对 信息

// 略  export class LinkTool {   // 略    pointsVisible(visible: boolean, group?: Konva.Group) {     if (group) {       this.pointsVisibleEach(visible, group)     } else {       const groups = this.render.layer.find('.asset') as Konva.Group[]       for (const group of groups) {         this.pointsVisibleEach(visible, group)       }     }      // 更新连线     this.render.draws[Draws.LinkDraw.name].draw()     // 更新预览     this.render.draws[Draws.PreviewDraw.name].draw()   }    remove(line: Konva.Line) {     const { groupId, pointId, pairId } = line.getAttrs()     if (groupId && pointId && pairId) {       const group = this.render.layer.findOne(`#${groupId}`) as Konva.Group       if (group) {         const points = (group.getAttr('points') ?? []) as LinkDrawPoint[]         const point = points.find((o) => o.id === pointId)         if (point) {           const pairIndex = (point.pairs ?? ([] as LinkDrawPair[])).findIndex(             (o) => o.id === pairId           )           if (pairIndex > -1) {             point.pairs.splice(pairIndex, 1)             group.setAttr('points', points)              // 更新连线             this.render.draws[Draws.LinkDraw.name].draw()             // 更新预览             this.render.draws[Draws.PreviewDraw.name].draw()           }         }       }     }   } } 

关键逻辑

  • 绘制 连接线(临时)
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

src/Render/draws/LinkDraw.ts

起点鼠标按下 'mousedown' -> 略 -> 终点鼠标释放 'mouseup'

// 略  export class LinkDraw extends Types.BaseDraw implements Types.Draw {   // 略    override draw() {     this.clear()      // stage 状态     const stageState = this.render.getStageState()      const groups = this.render.layer.find('.asset') as Konva.Group[]      const points = groups.reduce((ps, group) => {       return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])     }, [] as LinkDrawPoint[])      const pairs = points.reduce((ps, point) => {       return ps.concat(point.pairs ? point.pairs : [])     }, [] as LinkDrawPair[])      // 略      // 连接点     for (const point of points) {       const group = groups.find((o) => o.id() === point.groupId)        // 非 选择中       if (group && !group.getAttr('selected')) {         const anchor = this.render.layer.findOne(`#${point.id}`)          if (anchor) {           const circle = new Konva.Circle({             id: point.id,             groupId: group.id(),             x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),             y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),             radius: this.render.toStageValue(this.option.size),             stroke: 'rgba(255,0,0,0.2)',             strokeWidth: this.render.toStageValue(1),             name: 'link-point',             opacity: point.visible ? 1 : 0           })            // 略            circle.on('mousedown', () => {             this.render.selectionTool.selectingClear()              const pos = this.render.stage.getPointerPosition()              if (pos) {               // 临时 连接线 画               this.state.linkingLine = {                 group: group,                 circle: circle,                 line: new Konva.Line({                   name: 'linking-line',                   points: _.flatten([                     [circle.x(), circle.y()],                     [                       this.render.toStageValue(pos.x - stageState.x),                       this.render.toStageValue(pos.y - stageState.y)                     ]                   ]),                   stroke: 'blue',                   strokeWidth: 1                 })               }                this.layer.add(this.state.linkingLine.line)             }           })            // 略       }     }   } }  

src/Render/handlers/LinkHandlers.ts

拖动显示线条、移除 连接线(临时)

从起点到鼠标当前位置

  handlers = {     stage: {       mouseup: () => {         const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state          // 临时 连接线 移除         linkDrawState.linkingLine?.line.remove()         linkDrawState.linkingLine = null       },       mousemove: () => {         const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state          const pos = this.render.stage.getPointerPosition()          if (pos) {           // stage 状态           const stageState = this.render.getStageState()            // 临时 连接线 画           if (linkDrawState.linkingLine) {             const { circle, line } = linkDrawState.linkingLine             line.points(               _.flatten([                 [circle.x(), circle.y()],                 [                   this.render.toStageValue(pos.x - stageState.x),                   this.render.toStageValue(pos.y - stageState.y)                 ]               ])             )           }         }       }     }   } 
  • 产生连接信息

src/Render/draws/LinkDraw.ts

// 略  export class LinkDraw extends Types.BaseDraw implements Types.Draw {   // 略    override draw() {     this.clear()      // stage 状态     const stageState = this.render.getStageState()      const groups = this.render.layer.find('.asset') as Konva.Group[]      const points = groups.reduce((ps, group) => {       return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])     }, [] as LinkDrawPoint[])      const pairs = points.reduce((ps, point) => {       return ps.concat(point.pairs ? point.pairs : [])     }, [] as LinkDrawPair[])      // 略      // 连接点     for (const point of points) {       const group = groups.find((o) => o.id() === point.groupId)        // 非 选择中       if (group && !group.getAttr('selected')) {         const anchor = this.render.layer.findOne(`#${point.id}`)          if (anchor) {           const circle = new Konva.Circle({             id: point.id,             groupId: group.id(),             x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),             y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),             radius: this.render.toStageValue(this.option.size),             stroke: 'rgba(255,0,0,0.2)',             strokeWidth: this.render.toStageValue(1),             name: 'link-point',             opacity: point.visible ? 1 : 0           })            // 略            circle.on('mouseup', () => {             if (this.state.linkingLine) {               const line = this.state.linkingLine               // 不同连接点               if (line.circle.id() !== circle.id()) {                 const toGroup = groups.find((o) => o.id() === circle.getAttr('groupId'))                  if (toGroup) {                   const fromPoints = (                     Array.isArray(line.group.getAttr('points')) ? line.group.getAttr('points') : []                   ) as LinkDrawPoint[]                    const fromPoint = fromPoints.find((o) => o.id === line.circle.id())                    if (fromPoint) {                     const toPoints = (                       Array.isArray(toGroup.getAttr('points')) ? toGroup.getAttr('points') : []                     ) as LinkDrawPoint[]                      const toPoint = toPoints.find((o) => o.id === circle.id())                      if (toPoint) {                       if (Array.isArray(fromPoint.pairs)) {                         fromPoint.pairs = [                           ...fromPoint.pairs,                           {                             id: nanoid(),                             from: {                               groupId: line.group.id(),                               pointId: line.circle.id()                             },                             to: {                               groupId: circle.getAttr('groupId'),                               pointId: circle.id()                             }                           }                         ]                       }                        // 更新历史                       this.render.updateHistory()                       this.draw()                       // 更新预览                       this.render.draws[Draws.PreviewDraw.name].draw()                     }                   }                 }               }                // 临时 连接线 移除               this.state.linkingLine?.line.remove()               this.state.linkingLine = null             }           })            this.group.add(circle)         }          // 略       }     }   } }  
  • 绘制 连接线
    前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

src/Render/draws/LinkDraw.ts

这里就是利用了上面提到的 连接点(锚点),通过它的 absolutePosition 获得真实位置。

// 略  export class LinkDraw extends Types.BaseDraw implements Types.Draw {   // 略    override draw() {     this.clear()      // stage 状态     const stageState = this.render.getStageState()      const groups = this.render.layer.find('.asset') as Konva.Group[]      const points = groups.reduce((ps, group) => {       return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])     }, [] as LinkDrawPoint[])      const pairs = points.reduce((ps, point) => {       return ps.concat(point.pairs ? point.pairs : [])     }, [] as LinkDrawPair[])      // 连接线     for (const pair of pairs) {       const fromGroup = groups.find((o) => o.id() === pair.from.groupId)       const fromPoint = points.find((o) => o.id === pair.from.pointId)        const toGroup = groups.find((o) => o.id() === pair.to.groupId)       const toPoint = points.find((o) => o.id === pair.to.pointId)        if (fromGroup && toGroup && fromPoint && toPoint) {         const fromAnchor = this.render.layer.findOne(`#${fromPoint.id}`)         const toAnchor = this.render.layer.findOne(`#${toPoint.id}`)          if (fromAnchor && toAnchor) {           const line = new Konva.Line({             name: 'link-line',             // 用于删除连接线             groupId: fromGroup.id(),             pointId: fromPoint.id,             pairId: pair.id,             //             points: _.flatten([               [                 this.render.toStageValue(fromAnchor.absolutePosition().x - stageState.x),                 this.render.toStageValue(fromAnchor.absolutePosition().y - stageState.y)               ],               [                 this.render.toStageValue(toAnchor.absolutePosition().x - stageState.x),                 this.render.toStageValue(toAnchor.absolutePosition().y - stageState.y)               ]             ]),             stroke: 'red',             strokeWidth: 2           })           this.group.add(line)            // 连接线 hover 效果           line.on('mouseenter', () => {             line.stroke('rgba(255,0,0,0.6)')             document.body.style.cursor = 'pointer'           })           line.on('mouseleave', () => {             line.stroke('red')             document.body.style.cursor = 'default'           })         }       }     }      // 略   } }  
  • 绘制 连接点

src/Render/draws/LinkDraw.ts

// 略  export class LinkDraw extends Types.BaseDraw implements Types.Draw {  // 略    override draw() {     this.clear()      // stage 状态     const stageState = this.render.getStageState()      const groups = this.render.layer.find('.asset') as Konva.Group[]      const points = groups.reduce((ps, group) => {       return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])     }, [] as LinkDrawPoint[])      const pairs = points.reduce((ps, point) => {       return ps.concat(point.pairs ? point.pairs : [])     }, [] as LinkDrawPair[])      // 略      // 连接点     for (const point of points) {       const group = groups.find((o) => o.id() === point.groupId)        // 非 选择中       if (group && !group.getAttr('selected')) {         const anchor = this.render.layer.findOne(`#${point.id}`)          if (anchor) {           const circle = new Konva.Circle({             id: point.id,             groupId: group.id(),             x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),             y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),             radius: this.render.toStageValue(this.option.size),             stroke: 'rgba(255,0,0,0.2)',             strokeWidth: this.render.toStageValue(1),             name: 'link-point',             opacity: point.visible ? 1 : 0           })            // hover 效果           circle.on('mouseenter', () => {             circle.stroke('rgba(255,0,0,0.5)')             circle.opacity(1)             document.body.style.cursor = 'pointer'           })           circle.on('mouseleave', () => {             circle.stroke('rgba(255,0,0,0.2)')             circle.opacity(0)             document.body.style.cursor = 'default'           })            // 略       }     }   } }  
  • 复制

有几个关键:

  1. 更新 id,包括:节点、连接点、锚点、连接对
  2. 重新绑定相关事件

src/Render/tools/CopyTool.ts

// 略  export class CopyTool {   // 略    /**    * 复制粘贴    * @param nodes 节点数组    * @param skip 跳过检查    * @returns 复制的元素    */   copy(nodes: Konva.Node[]) {     const clones: Konva.Group[] = []      for (const node of nodes) {       if (node instanceof Konva.Transformer) {         // 复制已选择         const backup = [...this.render.selectionTool.selectingNodes]         this.render.selectionTool.selectingClear()         this.copy(backup)          return       } else {         // 复制未选择(先记录,后处理)         clones.push(node.clone())       }     }      // 处理克隆节点      // 新旧 id 映射     const groupIdChanges: { [index: string]: string } = {}     const pointIdChanges: { [index: string]: string } = {}      // 新 id、新事件     for (const copy of clones) {       const gid = nanoid()       groupIdChanges[copy.id()] = gid       copy.id(gid)        const pointsClone = _.cloneDeep(copy.getAttr('points') ?? [])       copy.setAttr('points', pointsClone)        for (const point of pointsClone) {         const pid = nanoid()         pointIdChanges[point.id] = pid          const anchor = copy.findOne(`#${point.id}`)         anchor?.id(pid)          point.id = pid          point.groupId = copy.id()         point.visible = false       }        copy.off('mouseenter')       copy.on('mouseenter', () => {         // 显示 连接点         this.render.linkTool.pointsVisible(true, copy)       })       copy.off('mouseleave')       copy.on('mouseleave', () => {         // 隐藏 连接点         this.render.linkTool.pointsVisible(false, copy)          // 隐藏 hover 框         copy.findOne('#hoverRect')?.visible(false)       })        // 使新节点产生偏移       copy.setAttrs({         x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,         y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount       })     }      // pairs 新 id     for (const copy of clones) {       const points = copy.getAttr('points') ?? []       for (const point of points) {         for (const pair of point.pairs) {           // id 换新           pair.id = nanoid()           pair.from.groupId = groupIdChanges[pair.from.groupId]           pair.from.pointId = pointIdChanges[pair.from.pointId]           pair.to.groupId = groupIdChanges[pair.to.groupId]           pair.to.pointId = pointIdChanges[pair.to.pointId]         }       }     }      // 略   } }  

接下来,计划实现下面这些功能:

  • 连接线 - 折线(头疼)
  • 等等。。。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

发表评论

评论已关闭。

相关文章