工业仿真(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 图编辑引擎

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

相关推荐
非凡ghost1 小时前
MPC-BE视频播放器(强大视频播放器) 中文绿色版
前端·windows·音视频·软件需求
Stanford_11061 小时前
React前端框架有哪些?
前端·微信小程序·前端框架·微信公众平台·twitter·微信开放平台
洛可可白1 小时前
把 Vue2 项目“黑盒”嵌进 Vue3:qiankun 微前端实战笔记
前端·vue.js·笔记
学习同学2 小时前
从0到1制作一个go语言游戏服务器(二)web服务搭建
服务器·前端·golang
-D调定义之崽崽2 小时前
【初学】调试 MCP Server
前端·mcp
四月_h2 小时前
vue2动态实现多Y轴echarts图表,及节点点击事件
前端·javascript·vue.js·echarts
文心快码BaiduComate3 小时前
用Zulu轻松搭建国庆旅行4行诗网站
前端·javascript·后端
行者..................4 小时前
手动编译 OpenCV 4.1.0 源码,生成 ARM64 动态库 (.so),然后在 Petalinux 中打包使用。
前端·webpack·node.js
小爱同学_5 小时前
一次面试让我重新认识了 Cursor
前端·面试·程序员
golang学习记5 小时前
AI 乱写代码?不是模型不行,而是你的 VS Code 缺了 Context!MCP 才是破局关键
前端