工业仿真(simulation)--前端(四)--画布编辑(2)

在这篇文章中,我重点讲解画布实例的生成

生成div

首先需要有一个div框架,在我的组件中我设置了两个画布,一个是背景画布,一个是可操作画布,背景画布主要显示网格,这样做的好处就是既显示网格,又提供了精度

图一

html代码如下

html 复制代码
<div id="bgCanvas" class="bg-canvas"></div>
<div
    id="operateCanvas"
    class="operate-canvas"
    @keydown="spaceKeyDownEvents($event)"
    @keyup="spaceKeyUpEvents($event)"
></div>

id为bgCanvas的就是背景画布,另一个就是操作画布,后面会介绍两个画布怎么进行同步,包括缩放和位移

创建实例

然后在ts中为这两个div注册画布

ts 复制代码
//初始化画布实例
export const initCanvas = (): void => {
  //设置背景画布
  bgCanvas = new Graph({
    container: document.getElementById('bgCanvas') as HTMLElement,
    autoResize: true,
    grid: {
      type: 'doubleMesh',
      size: 10,
      visible: true,
      args: [
        {
          color: '#e0e0e080',
          thickness: 1
        },
        {
          color: '#e0e0e0',
          thickness: 1,
          factor: 10
        }
      ]
    },
    background: {
      color: '#f7f7f7'
    }
  })

  //设置画布
  canvas = new Graph({
    container: document.getElementById('operateCanvas') as HTMLElement,
    autoResize: true,
    //平移
    panning: {
      enabled: false,
      eventTypes: ['leftMouseDown']
    },
    //缩放
    mousewheel: {
      enabled: true,
      factor: 1.1
    },
    //网格
    grid: {
      size: 1,
      visible: true
    },

    connecting: {
      //是否可以连接到空白处
      allowBlank(args: any) {
        const shape = args.edge.shape
        if (shape === 'ordinary-edge') {
          return false
        }
        return true
      },

      //是否可以自己连自己
      allowLoop(args: any) {
        if (args.edge.shape === 'road-edge') {
          return true
        }

        if (args.sourceCell.id !== args.targetCell.id) {
          return true
        } else {
          return false
        }
      },

      //是否允许连接到节点(非连接桩)
      allowNode() {
        return false
      },

      allowEdge() {
        return true
      },

      //是否允许创建相同连线
      allowMulti: 'withPort',

      // 吸附半径
      snap: {
        radius: 15
      },

      //创建连接线
      createEdge({ sourceCell }) {
        if (sourceCell.shape === 'cross-node') {
          return this.addEdge({
            shape: 'road-edge'
          })
        } else {
          return this.addEdge({
            shape: 'ordinary-edge'
          })
        }
      },

      //点击连接桩时,先判断是否需要新增连接边
      validateMagnet({ cell, magnet }) {
        if (this.isNode(cell)) {
          const node = cell as Node
          const ports = node.getPorts()
          //获取magnet中port属性
          const portId = magnet.getAttribute('port')
          const targetPort = ports.find((port) => port.id === portId)
          const prohibitPorts = ['topIn', 'bottomIn', 'leftIn', 'rightIn']
          if (prohibitPorts.includes(targetPort?.group || '')) {
            return false
          }
        }
        return true
      },
      //验证是否可以连接
      validateConnection({ edge, targetMagnet }) {
        if (edge?.shape === 'road-edge') {
          return true
        }

        const prohibitPorts = ['topOut', 'bottomOut', 'leftOut', 'rightOut']
        const targetPort = targetMagnet?.getAttribute('port-group')
        if (prohibitPorts.includes(targetPort || '')) {
          return false
        }
        return !!targetMagnet
      }
    },

    interacting: (cellView: CellView) => {
      const { isCanvasNodeInteractive, isEdgeMovable } = storeToRefs(useCanvasParameterStore())
      if (!isCanvasNodeInteractive.value) {
        return false
      }

      const { lockNodeIds } = storeToRefs(useLayerStore())
      if (lockNodeIds.value.includes(cellView.cell.id)) {
        return false
      }

      return {
        edgeMovable: isEdgeMovable.value
      }
    }
  })

  //使用插件
  //键盘快捷键
  canvas.use(
    new Keyboard({
      enabled: true
    })
  )
  //框选
  canvas.use(
    new Selection({
      rubberband: true,
      eventTypes: ['leftMouseDown'],
      showNodeSelectionBox: true
    })
  )
  //对齐线
  canvas.use(
    new Snapline({
      enabled: false,
      sharp: true,
      tolerance: 5,
      filter: (node: Node) => {
        if (snaplineNodeShape.includes(node.shape)) {
          return true
        } else {
          return false
        }
      }
    })
  )
  //图形变换
  canvas.use(
    new Transform({
      resizing: {
        enabled: () => {
          const { isGraphTransform } = useCanvasParameterStore()
          return isGraphTransform
        },
        minWidth: 10,
        minHeight: 0,
        orthogonal: false,
        restrict: false,
        preserveAspectRatio: false
      }
    })
  )

  dnd = new Dnd({
    //canvas是画布组件实例
    target: canvas,
    //拖拽结束时的回调函数
    validateNode(droppingNode: Node): boolean {
      //给节点设置连接桩
      setNodePort(droppingNode)
      //渲染进程给主进程发送消息,添加节点
      renderAddNode(droppingNode)
      //返回false,这里不添加节点
      return false
    }
  })
}

initCanvas方法可以在onMounted钩子函数里面直接调用即可

到这一步,我们的画布就显示出来了,但是我们创建的画布缺少交互,就比如上面提到的,背景画布和操作画布怎么进行同步,那下面我们就给我们的画布实例添加"五感"

创建监听事件

canvas是操作画布,bgCanvas是背景画布

ts 复制代码
//画布监听事件
export const initCanvasEvents = (): void => {
  //画布-缩放事件
  canvas.on('scale', () => {
    //获取canvas的缩放级别
    const zoom = canvas.zoom()
    bgCanvas.zoom(zoom, { absolute: true })
    const { canvasScale } = storeToRefs(useCanvasParameterStore())
    canvasScale.value = zoom

    //给画布操作发消息
    useMessageStore().addCanvasOperate({
      source: '画布缩放',
      content: `当前缩放比例`,
      otherMsg: zoom.toFixed(2)
    })
    prohibitCanvasMoveMsg = false
  })

  //画布-移动事件
  canvas.on('translate', ({ tx, ty }: { tx: number; ty: number }) => {
    const { canvasPosition } = storeToRefs(useCanvasParameterStore())
    canvasPosition.value.x = -tx
    canvasPosition.value.y = -ty

    bgCanvas.translate(tx, ty)
    if (prohibitCanvasMoveMsg) {
      //给画布操作发消息
      useMessageStore().addCanvasOperate({
        source: '画布移动',
        content: `当前画布位置`,
        otherMsg: `x:${-tx.toFixed(2)},y:${-ty.toFixed(2)}`
      })
    } else {
      prohibitCanvasMoveMsg = true
    }
  })
}

在上面代码中,我们可以看到,操作画布监听了两个事件

  1. 缩放事件
  2. 位移事件

当我们收到这两个事件后,我们会立即同步到背景画布中,当然initCanvasEvents方法也需要在onMounted钩子函数里面直接调用,那类似于这种的事件还有很多,如图二图三所示

图二

图三

到此我们的画布实例就彻底完成,接下来就是美化我们创建的节点

创建Cell

这一步主要是为了美化我们创建的节点,对于美术功底不太好的同学来说可能会比较难,下面先展示效果,如图四,各个节点在画布中的效果

图四

接下来是创建这些节点的源代码

ts 复制代码
//注册节点与连接线
export const registerNodeAndEdge = (): void => {
  //注册普通节点,长100,宽100
  register({
    shape: 'ordinary-node',
    width: 100,
    height: 100,
    component: OrdinaryNode,
    ports: {
      groups: customPorts
    }
  })

  //创建产品节点
  register({
    shape: 'product-node',
    width: 20,
    height: 20,
    component: ProductNode
  })

  //注册十字路节点
  register({
    shape: 'cross-node',
    width: 120,
    height: 120,
    component: Crossing,
    ports: {
      groups: customPorts
    }
  })

  //创建搬运流节点
  register({
    shape: 'transport-node',
    width: 100,
    height: 100,
    component: TransportNode
  })

  //注册添加事件的节点
  register({
    shape: 'add-node',
    width: 18,
    height: 18,
    component: AddNode,
    attrs: {
      body: {
        magnet: true
      }
    }
  })

  //自定义顶点区域
  Graph.registerNode(
    'custom-port-node',
    {
      inherit: 'polygon',
      width: 100,
      height: 100,
      attrs: {
        body: {
          fill: '#d6ddff66',
          stroke: 'transparent',
          points: '0,0 100,0 100,100 0,100',
          event: 'customPortNodeClick'
        }
      }
    },
    true
  )

  //注册编辑多边形的顶点小圆圈
  Graph.registerNode(
    'free-edit-circle',
    {
      inherit: 'circle',
      width: 4,
      height: 4,
      attrs: {
        body: {
          fill: '#fff',
          stroke: '#0087dc',
          strokeWidth: 1
        }
      }
    },
    true
  )
  //注册编辑多边形的边
  Graph.registerEdge(
    'free-edit-edge',
    {
      inherit: 'edge',
      markup: [
        {
          tagName: 'path',
          selector: 'p1'
        },
        {
          tagName: 'path',
          groupSelector: 'arrow',
          selector: 'a1'
        }
      ],
      attrs: {
        p1: {
          stroke: '#0087dc',
          strokeWidth: 0.7,
          connection: true,
          fill: 'none'
        },
        a1: {
          d: 'M -2 0 0 -2 2 0 0 2 z',
          fill: '#fff',
          stroke: '#ED8A19',
          atConnectionRatio: 0.5,
          cursor: 'pointer',
          event: 'freeEditEdgeClick'
        }
      }
    },
    true
  )

  //注册纯文本节点
  Graph.registerNode(
    'text-node',
    {
      inherit: 'rect',
      width: 100,
      height: 25,
      markup: [
        {
          tagName: 'rect',
          selector: 'body'
        },

        {
          tagName: 'text',
          selector: 'label'
        }
      ],
      attrs: {
        rect: {
          fill: 'transparent',
          stroke: 'transparent'
        },
        label: {
          text: '自定义文字',
          fill: '#4E5969',
          fontSize: 15,
          fontWeight: 'regular',
          letterSpacing: 0,
          event: 'textNodeClick'
        }
      }
    },
    true
  )

  //注册普通连接线
  Graph.registerEdge(
    'ordinary-edge',
    {
      inherit: 'edge',
      anchor: 'center',
      connectionPoint: 'anchor',
      markup: [
        {
          tagName: 'path',
          selector: 'p1'
        },
        {
          tagName: 'path',
          groupSelector: 'arrow',
          selector: 'a1'
        }
      ],
      attrs: {
        p1: {
          connection: true,
          fill: 'none',
          stroke: '#A2B1C3',
          strokeWidth: 2,
          strokeLinejoin: 'round'
        },
        a1: {
          d: 'M 0 -6 8 0 0 6 z',
          fill: '#ED8A19',
          stroke: '#ED8A19',
          atConnectionRatio: 0.5,
          cursor: 'pointer'
        }
      }
    },
    true
  )

  //注册传送带鼠标拖下来的样式节点
  Graph.registerNode(
    'edge-drag-node',
    {
      inherit: 'rect',
      width: 150,
      height: 20,
      markup: [
        {
          tagName: 'rect',
          selector: 'body'
        },

        {
          tagName: 'text',
          selector: 'label'
        }
      ],
      attrs: {
        rect: {
          fill: 'transparent',
          stroke: '#c0c0c0',
          strokeWidth: 1
        },
        label: {
          text: '',
          fill: '#4E5969',
          fontSize: 13,
          textAnchor: 'middle'
        }
      }
    },
    true
  )

  //注册传送带
  Graph.registerEdge(
    'conveyor-belt',
    {
      inherit: 'edge',
      router: {
        name: 'orth'
      },
      connector: { name: 'rounded' },
      anchor: 'center',
      connectionPoint: 'anchor',
      markup: [
        {
          tagName: 'rect',
          selector: 'port'
        },
        {
          tagName: 'path',
          selector: 'p1'
        },
        {
          tagName: 'path',
          selector: 'p2'
        },
        {
          tagName: 'path',
          selector: 'p3'
        },
        {
          tagName: 'text',
          selector: 'describe'
        }
      ],
      attrs: {
        port: {
          y: -7,
          connection: true,
          width: 5,
          height: 15,
          fill: '#c0c0c0',
          atConnectionRatio: 0,
          magnet: true
        },
        p1: {
          connection: true,
          fill: 'none',
          stroke: '#c0c0c0',
          strokeWidth: 20,
          strokeLinejoin: 'round',
          event: 'conveyorClick'
        },
        p2: {
          connection: true,
          fill: 'none',
          stroke: '#f5f8ff',
          strokeWidth: 17,
          strokeLinejoin: 'round',
          event: 'conveyorClick'
        },
        p3: {
          connection: true,
          fill: 'none',
          stroke: '#c0c0c0',
          strokeWidth: 20,
          pointerEvents: 'none',
          strokeLinejoin: 'round',
          strokeDasharray: '1,15',
          event: 'conveyorClick'
        },
        describe: {
          atConnectionRatio: 0.5,
          textAnchor: 'middle',
          textVerticalAnchor: 'middle',
          fontSize: 13,
          fill: '#0087dc',
          text: '➤'
        }
      }
    },
    true
  )

  //注册道路连线
  Graph.registerEdge(
    'road-edge',
    {
      inherit: 'edge',
      router: {
        name: 'orth'
      },
      connector: { name: 'rounded' },
      anchor: 'center',
      connectionPoint: 'anchor',
      markup: [
        {
          tagName: 'path',
          selector: 'p1'
        },
        {
          tagName: 'path',
          selector: 'p2'
        }
      ],
      attrs: {
        p1: {
          connection: true,
          fill: 'none',
          stroke: '#A2B1C3',
          strokeWidth: 20,
          strokeLinejoin: 'round',
          event: 'roadEdgeClick'
        },
        p2: {
          connection: true,
          fill: 'none',
          stroke: '#d9c956',
          strokeWidth: 3,
          pointerEvents: 'none',
          strokeLinejoin: 'round',
          strokeDasharray: 5,
          event: 'roadEdgeClick'
        }
      }
    },
    true
  )

  //注册警戒线
  Graph.registerEdge(
    'warning-line',
    {
      inherit: 'edge',
      router: {
        name: 'orth'
      },
      connector: { name: 'rounded' },
      anchor: 'center',
      connectionPoint: 'anchor',
      markup: [
        {
          tagName: 'path',
          selector: 'p1'
        },
        {
          tagName: 'path',
          selector: 'p2'
        },
        {
          tagName: 'path',
          selector: 'p3'
        }
      ],
      attrs: {
        p1: {
          connection: true,
          fill: 'none',
          stroke: '#f5cf36ff',
          strokeWidth: 12,
          strokeLinejoin: 'round'
        },
        p2: {
          connection: true,
          fill: 'none',
          stroke: '#ffd83b',
          strokeWidth: 8,
          strokeDasharray: '10,10',
          strokeDashoffset: 0,
          strokeLinejoin: 'round'
        },
        p3: {
          connection: true,
          fill: 'none',
          stroke: '#3f4347',
          strokeWidth: 8,
          strokeDasharray: '10,10',
          strokeDashoffset: 10,
          strokeLinejoin: 'round'
        }
      }
    },
    true
  )
}

然后我把官网的一些示例推荐给大家,方便大家的理解

Vue 节点 | X6 图编辑引擎

到此,我们的画布就正式的创建完毕,谢谢大家,在下一篇文章中,我会给大家介绍一个重要的组件,【标尺-刻度尺】,也是完全自主研发

相关推荐
an__ya__3 小时前
Vue数据响应式reactive
前端·javascript·vue.js
苦逼的搬砖工3 小时前
Flutter UI Components:闲来无事,设计整理了这几年来使用的UI组件库
前端·flutter
想买Rolex和Supra的凯美瑞车主3 小时前
Taro + Vite 开发中 fs.allow 配置问题分析与解决
前端
ruanCat3 小时前
使用 vite 的 base 命令行参数来解决项目部署在 github page 的路径问题
前端·github
Codebee3 小时前
使用Qoder 改造前端UI/UE升级改造实践:从传统界面到现代化体验的华丽蜕变
前端·人工智能
叫我詹躲躲3 小时前
开发提速?Vue3模板隐藏技巧来了
前端·vue.js·ai编程
华仔啊3 小时前
面试都被问懵了?CSS 的 flex:1 和 flex:auto 真不是一回事!90%的人都搞错了
前端·javascript
前端康师傅3 小时前
JavaScript 函数详解
前端·javascript
金金金__3 小时前
antd v5 support React is 16 ~ 18. see https://u.ant.design/v5-for-19 for...
前端