可视化引擎 引入自定义图元库详解

概要

可视化编辑器已成为前端发展趋势,相关产品层出不穷,但是用户较难根据自身需求去完整实现一个功能较为全面的可视化编辑器,我将采用乐吾乐开源的meta2d.js可视化库来实现一个简单的流程图编辑器,通过这个案例来介绍meta2d的相关功能,并向读者展示如何用meta2d从零出发搭建一个较为完整的项目,让我们在实际项目中来体验meta2d的强大之处吧。

$MUTB]6JJ667KR$SC95VH(1.png

什么是乐吾乐meta2d.js

meta2d是乐吾乐开源的2D图元组成的可视化引擎,集实时数据展示、动态交互、数据管理等一体的全功能2D可视化引擎。能够快速实现数字孪生、大屏可视化、Web组态、SCADA等解决方案。具有实时监控、多样、变化、动态交互、高效、可扩展、支持自动算法、跨平台等特点,最大程度减少研发和运维的成本,并致力于普通业务人员 0 代码开发实现物联网、工业互联网、电力能源、水利工程、智慧农业、智慧医疗、智慧城市等可视化解决方案。

乐吾乐已将其meta2d核心库完全免费开源,本系列教程就是基于meta2d从零实现web端可视化流程图编辑器。

乐吾乐 meta2d开源项目地址:https://github.com/le5le-com/meta2d.js

乐吾乐 meta2d官方文档:https://doc.le5le.com/document/119359590

项目地址

此可视化流程图编辑器项目地址:github.com/Grnetsky/me…

在线体验地址: http://editor.xroot.top/

往期教程

  1. 基本环境搭建: juejin.cn/spost/72617…
  2. 主界面布局及其初始化: juejin.cn/post/726219…
  3. Meta2d核心库图元注册流程及相关概念: juejin.cn/spost/72629…
  4. 侧边栏功能开发:https://juejin.cn/post/7264414580776403003
  5. Nav组件功能实现:https://juejin.cn/post/7264951443344916517
  6. Nav组件扩展-添加工具栏:https://juejin.cn/post/7265692989611147283
  7. setting组件框架搭建及其Map组件实现:https://juejin.cn/post/7267418197728116748

8.引入自定义图元库详解

写了这么久,发现之前有个重要的部分遗漏了,所以今天咱们先不讲meta2d引擎的全局配置,在之前我们讲解了如何引入并使用meta2d的图元扩展库和内置图元库,但是我们还不能实现使用自己的自定义图元库,这一章我们将讲解如何引入支持多种格式(icon、svg、png、path2D、canvas原生)的自定义图元库。

注意这一章只是教你如何引入自定义图元,而设计自定义图元不在本系列文章教学范围之内。

基本框架搭建

由于我们要引入的自定义图元库类别较多,所以我们需要分类进行讨论,在这里首先先把基础架构搭建出来,还记得我们引入的默认图形库的三步走吗?安装、注册、配置侧边栏图元信息。在这里也是同样的,只不过将安装变为将我们自己的图元库的路径配置在meta2d的相关函数中,而对于特殊的图元库比如svg、png、icon这样的,meta2d内部已经实现了对这些格式数据的解析,我们只需要告知meta2d他们的格式和相关属性就好,引入工作就更简单啦。

万事开头难,我们先找到Icon组件,其中我们的图元都是从defaultIcons中来的,这里我们要改成iconList,他由defaultIcons和getOtherIcons组成,接下来,我们就要在getOtherIcons函数中实现对自定义图元的引入工作了。

image.png

在icons.js中新建getOtherIcons函数,然后其中包含五个子函数,分别对应了我们五个需要导入的图元类型,然后定义好各个函数,如下

image.png

接下来,我们就开始今天的教程吧。

引入svg和png自定义库

首先在这给出官方教程的通道

先别着急写代码,我们先探讨下理想状态下要实现什么样的功能,我们的自定义图元库可以通过请求后端得到,也可以直接嵌入到前端代码中,在这里主要讲解嵌入到前端项目中的方法,这个方法掌握好了后,加载网络请求的自定义图元库就很简单了。嵌入到前端项目代码中也分很多种方案,一种方案是拖入文件后,告诉项目应该去哪儿加载文件,另一种方案就是项目自动去请求某个路径下的所有文件,只需要我们不断的拖入相关文件,不需要进行任何配置,这种方案在日常管理使用中十分方便简洁,所以我们主要讲解这种方案。

首先,将我们的自定义图元库放在public文件夹下

image.png

然后需要vite主动去检索我们的文件夹,如何达到这一目的?既然是文件操作,就离不开fs模块,一种解决方案是,通过扩展vite的devsServer服务器,向其中插入中间件拦截其网络请求,然后根据请求的url调用fs模块去读取文件夹下的路径,再将数据返回回去,这样就实现了文件夹的目录读取。

特别注意!!! 该方案在项目打包上线后需要配置后端相关接口才能生效,如果不想过于依赖后端,你可以通过一个json文件去保存图元文件路径,这里就不做演示了。

vite插件文档

开始动手,在vite.config.js中,我们配置如下:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import fs from "fs"
import path from  "path"
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(),
      devServerMiddleware()],
})

function devServerMiddleware(){
  return {
    name:"vite-plugin-get-files",  // 插件名,标准配置
    configureServer(server){  // 方法
      server.middlewares.use( // 引入中间间
          (req, res, next)=>{
            const url = req.originalUrl  // 获取请求的源路径
            if((url.startsWith("/svg/") || url.startsWith("/png/") || url.startsWith("/icon/") || url.startsWith("/canvasDraw/") || url.startsWith("/path2D/"))  && url.endsWith("/")){  // 路径判断 特殊处理svg和png路径的
                const pwd = decodeURI(path.join(__dirname, 'public', url));  // 路径
                try {
                    const files = fs.readdirSync(pwd, {  //  同步读取文件夹
                        withFileTypes: true,
                    });
                    const list = [];
                    for (const item of files) {
                        if (item.isDirectory()) {  // 判断是否为文件夹
                            list.push({ name: item.name, type: 'directory' });
                        } else {  // 非文件夹  则返回文件名  为了懒加载实现
                            list.push({ name: item.name });
                        }
                    }
                    res.end(JSON.stringify(list));
                }catch (e){
                    console.error(e)
                    next()
                }
            }else {
                next()  // 跳到下一步
            }
          }
      )
    }
  }
}

我们在plugins中引入了自己编写的插件,用于扩展devServer,通过判断请求路径,在逻辑函数中调用fs模块进行文件名的读取,需要说明的是由于实际开发中svg库的数量可能比较大,而且png格式的图片一般资源也比较大,若是用常规加载,会造成页面卡顿,加载白屏现象,故对于这种大文件的加载,我们采用懒加载形式,即用户打开文件夹才加载,然后将数据返回,这样就简单的实现了我们的需求,下一步要做的就是需要去设置网络请求,来请求这个路径。

此处的devServer就充当了后端的角色

为了方便未来对路径的扩充,我们先接收一个extend参数,以方便自定义填写其他路径

function getUserDir(path,extend = []){
  return async ()=>{
      const { data: fileList} = await axios.get(path)
      return fileList.concat(extend)  // 合并路径,方便未来用户自定义扩充路径
  }
}
export const userPensUrl = {
  "icon":getUserDir("/icon/"),
  "svg":getUserDir("/svg/"),
  "png":getUserDir("/png/"),
  "path2D":getUserDir("/path2D/"),
  "canvasDraw":getUserDir("/canvasDraw/")
}

在该对象的svg中编写获取路径逻辑函数,网络请求就放在这里面来做,最后将devServer传递过来的和自定义路径合并。

路径有了过后,我们需要做的事就简单了,设置网络请求获取数据,在getSvgs函数中,首先进行路径的获取,然后再将每个路径下的文件进行获取,像下面这样:

最终我们的数据就获取到了,来看看效果吧。

加载svg 00_00_00-00_00_30.gif

类似的,png也是完全一样的步骤,这里就不多赘述了。

image.png

image.png

现在,我们的文件夹虽然引入进去了,但是,暂时还没有将数据导入,接下来讲解如何进行懒加载导入数据。

作为懒加载,最关键的就是监听侧边栏被打开才开始加载数据,由于原生elementplus并没有提供监听侧边栏打开的事件,在这里的解决办法是通过监听click事件,然后在里面维状态,像下面这样:

<script setup>
import {
  defaultIcons,
  getOtherIcons,
  pngToPens,
  svgToPens,
} from "../data/icons.js";
import { Search } from "@element-plus/icons-vue";

import { onMounted, reactive, ref, watch } from "vue";
import axios from "axios";
import { parseSvg } from "@meta2d/svg";
const activeNames = ref(1);
const inputValue = ref("");
let iconList = reactive([...defaultIcons]);
onMounted(async () => {
  const icons = await getOtherIcons();
  iconList.push(...icons.flat(2));
});

function dragPen(data, e) {
  const json = JSON.stringify(data);
  e.dataTransfer.setData("meta2d", json);
}

async function changeState(tab) {
  if (tab.folder) {
    if (!tab.loaded) {
      const { data: files } = await axios.get(
        (tab.svg ? "/svg/" : "/png/") + tab.name + "/"
      );
      tab.loaded = true;
      if (tab.svg) {
        const fs = await Promise.all(files.map((f) => svgToPens(f, tab.name)));
        tab.list = fs;
      } else {
        const fs = await Promise.all(files.map((f) => pngToPens(f, tab.name)));
        tab.list = fs;
      }
    }
  }
}
</script>

<template>
  <div class="icons">
    <div class="icon_search">
      <el-input
        v-model="inputValue"
        size="small"
        placeholder="搜索图元"
        :prefix-icon="Search"
      />
    </div>
    <div class="icon_list">
      <el-collapse>
        <template v-for="icons in iconList">
          <el-collapse-item :title="icons.name" @click="changeState(icons)">
            <div class="icon_container">
              <div
                class="icon_item"
                v-for="(item, index) in icons.list"
                draggable="true"
                @dragstart="dragPen(item.data, $event)"
                :index="index"
              >
              <!-- 这里做了修改-->
               <svg v-if="item.icon" class="l-icon" aria-hidden="true">  
                <use :xlink:href="'#' + item.icon"></use>  
               </svg>
                <img v-else-if="item.image" :src="item.image" />
                <div v-else-if="item.svg" v-html="item.svg"></div>
              </div>
            </div>
          </el-collapse-item>
        </template>
      </el-collapse>
    </div>
    <div class="icon_manage">
      <el-button> 管理图元 </el-button>
    </div>
  </div>
</template>

注意 相比之前,我们将全局采用icon的Symbol格式来加载icon图标,所以列表渲染模板上有部分修改:将之前的i标签改为了现在的svg标签,下一节会讲到。

首先,我们通过监听click事件,在回调中判断该折叠栏中的数据类型是否为svg或者png格式,然后在判断是否已经被加载过,防止重复加载,若没有加载过,则先去对应的文件夹去请求所有文件路径,再分为svg和png单独处理,若为svg图元库,则调用svgToPen方法返回meta2d可识别的pen图元,其本质是调用了meta2d提供的parseSvg方法,代码如下:

// icons.js
export async function svgToPens(f, dName) {
  const name = getFileName(f.name)
  const image = "/svg/" + dName + "/" + f.name
  return {
    name,
    image,
    data: parseSvg(await fetch(image).then((res) => res.text())),
    component: true,
  }
}

最后将生成的图元返回给前端进行响应式显示。
最后,让我们来看看实际效果吧。

png演示 00_00_00-00_00_30.gif

另外需要说明的是,除了这么导入外,meta2d也支持直接拖拽导入,即需要相关svg或者png时直接拖拽到meta2d即可,他会自动解析,十分方便

引入icon图标

先放Meta2d官网文档,官网中引入的是iconfont的Fontclass格式,我们都知道iconfont支持多种格式,如果对此概念不清楚的话可以看看iconfont官网对该部分的描述,从官网中可以看到Symbol引用是iconfont官方推荐的做法,也是未来发展的主流,所以有别于meta2d官网,我这里采用Symbol引用方式引入icon图元。

icon图标的引入相对比较简单,下载iconfont的事就不说了,默认大家都会,第一步,将iconfont引入项目,在index.html中引入:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <script src="//at.alicdn.com/t/c/font_4042197_vr5c62twlzh.js"></script>
<!--      // 引入自定义icon图元-->
      <script src="/icon/myIcon_symbol/iconfont.js" ></script>
      <link rel="stylesheet" href="/icon/myIcon_symbol/iconfont.css">

      <style type="text/css">
          .l-icon {
              width: 2em;
              height: 2em;
              vertical-align: -0.15em;
              fill: currentColor;
              overflow: hidden;
          }
      </style>

      <title>Meta2d_vue</title>

  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>

<!--    引入ecahrts.min.js-->
    <script defer src="/js/echarts.min.js"></script>


<!--引入hightCharts-->
    <script defer src="/js/highcharts.js"></script>
    <script defer src="/js/highcharts.exporting.js"></script>
    <script defer src="/js/highcharts-more.js"></script>
    <script defer src="/js/lcjs.iife.js"></script>
    <script defer src="/js/canvas2svg.js"></script>
  </body>
</html>

上面代码中我们额外引入了iconfont.js和iconfont.css,iconfont.css才是meta2d能够加载识别的图元内容,iconfont.js是方便我们采用Symbol方式在侧边栏显示icon,所以要分清楚他们的作用。

第二步,配置我们的侧边栏,配置图元信息编写getUnicodeIcons函数,首先通过suerPenUrl.icon()获取检索目录,然后再通过addIcons加载图标,在addIcon中主要做两件事,一是进行网络请求获取iconfont.json信息,再将json信息写入到图元信息中心,相关代码如下所示:

export async function getOtherIcons() {
  let result = []
  let datas = await Promise.all([
    getUnicodeIcons(), // 引入字体图标  
    getSvgs(),
    getPngs(),
    getPath2Ds(),
    getCanvasDraw()
  ])
  result.push(...datas.filter(Boolean))
  return result
}
async function getUnicodeIcons() {
  let icons = []
  const iconsUrl = await userPensUrl.icon() // 获取字体图标的路径  
  icons = await Promise.all(
    iconsUrl.map(url => addIcons(url.name)) // 添加字体图标  
  )
  return icons // 返回结果  
}
async function addIcons(url){
  // 请求路径
 let data = await fetch("/icon/"+url+"/iconfont.json").then((rsp)=> rsp.json())
  let iconGroup = {
      name: data.name,
      loaded: true,
      show: true,
      list: [],
  }
    data.glyphs.map((item) =>
     iconGroup.list.push({  // 侧边栏信息
         name: item.name,
         icon:  // 侧边栏目显示
             data.css_prefix_text +
             item.font_class,
         data: {   // 配置图元信息
             width: 100,
             height: 100,
             name: 'icon',
             iconFamily: data.font_family, // 字体图标配置
             icon: String.fromCharCode(item.unicode_decimal),  // 字体图标
         },
     })
 )
  return iconGroup
}

这样,得益于我们之前搭好的框架,引入图标就极其简单了,我们只需要将相关文件准备好,拖入icon文件夹中,就可以了。

引入path2D

先放官方文档

meta2d支持直接绘画path2D函数,要自定义path2D图形库,也分为三步,第一步定义绘画函数和定义锚点函数(如果有的话),第二步,注册绘画函数和锚点函数,第三步引入到侧边栏。

首先来看第一步,首先在path2D文件夹下新建文件夹“mypath2D”,myTriangle.js文件,我们要在这里面定义我们要引入的path2D函数,跟官网一样,我们暂且画个三角形好了,代码如下:

// path2D/mypath2D/myTriangle.js

// path2d绘画函数  
export function myTriangle(pen, ctx) {
  const path = !ctx ? new Path2D() : ctx;
  const { x, y, width, height } = pen.calculative.worldRect; // 获取坐标  
  path.moveTo(x + width / 2, y);
  path.lineTo(x + width, y + height);
  path.lineTo(x, y + height);
  path.lineTo(x + width / 2, y);

  path.closePath();
  if (path instanceof Path2D) return path;
}

// path2d锚点注册函数,注意该位置是相对位置  
export function mtTriangleAnchors(pen) {
  const anchors = [];
  anchors.push({
    id: '0',
    penId: pen.id,
    x: 0.5,
    y: 0,
  });

  anchors.push({
    id: '1',
    penId: pen.id,
    x: 1,
    y: 1,
  });

  anchors.push({
    id: '2',
    penId: pen.id,
    x: 0,
    y: 1,
  });
  pen.anchors = anchors;
}

第二步,我们要在meta2d中注册他,像这样:

//注册自定义path2d图元  
meta2d.register({myTriangle})  
// 注册自定义图元的m锚点信息  
meta2d.registerAnchors({ myTriangle: myTriangleAnchors });

第三步,我们需要引入到侧边栏,在这里还是同样让他自动生成,我们去编写getPath2Ds函数,像之前编写的获取svg和png的函数一样,我们先指定fs去哪个文件夹读取文件,然后再调用网络请求,别忘了去vite.config.js中的devServermiddleware添加上请求拦截路径。
代码如下:

//getpath2Ds
async function getPath2Ds() {
  const folderName = "path2D/"
  let path2d = []
  const path2DUrl = await userPensUrl.path2D()
  for (let i of path2DUrl) {
    if (i.type === "directory") {
      const { data: files } = await axios.get(folderName + i.name + '/')
      let dataList = []
      for (let j of files) {
        const name = getFileName(j.name)
        dataList.push({
          name,
          icon: "t-icon t-" + iconMap[name], // 侧边栏展示图标  
          data: {
            width: 100,
            height: 100,
            name,
            text: "我的三角形"
          }
        })

      }
      path2d.push({
        name: i.name,
        count: files.length,
        list: dataList,
        show: true,
      })
    }
  }
  return path2d
}

上面的函数还是比较好理解的,需要注意的是,由于自定义path2d函数无展示图标,故使用了iconfont图标进行展示,需要时去iconfont官网搜索下载就是,然后在path2D文件夹中定义了index.js文件,其中导出处理iconMap对象,这是个path2d函数名与icon图标名的映射对象,引入新西定义path2D库时我们需要提前将映射关系补充在其中,然后在getPath2Ds函数中,会自动根据映射关系来获取展示图标,最终效果如下:

自定义path2d 00_00_00-00_00_30.gif

引入canvasDraw

canvasDraw即Canvas Context2D,是调用canvas原生api进行绘制的图形绘画函数,由于原生支持自定义效果更佳,下面来讲解引入方法。

其实引入方法与path2d一样的,先定义,再注册,再导入。

第一步 定义:

// public/canvasDraw/myCanvasDraw/canvasTriangle.js
export function canvasTriangle(ctx, pen) {
  // 在绘画中若更改了 ctx 的某个属性,例如:fillStyle, strokeStyle, lineWidth 等样式属性,需使用 save 和 restore  
  // 注意 save restore 需要成对调用  
  ctx.save();
  // 若在绘画函数中,配置了 ctx.strokeStyle or fillStyle ,那么画笔的 color or background 无法对它生效  
  ctx.strokeStyle = '#1890ff';
  ctx.moveTo(pen.calculative.worldRect.x + pen.calculative.worldRect.width / 2, pen.calculative.worldRect.y);
  ctx.lineTo(
    pen.calculative.worldRect.x + pen.calculative.worldRect.width,
    pen.calculative.worldRect.y + pen.calculative.worldRect.height
  );
  ctx.lineTo(pen.calculative.worldRect.x, pen.calculative.worldRect.y + pen.calculative.worldRect.height);
  ctx.lineTo(pen.calculative.worldRect.x + pen.calculative.worldRect.width / 2, pen.calculative.worldRect.y);

  ctx.closePath();
  ctx.stroke();
  // 若需要填充 ctx.fill();  

  ctx.restore();
}

export function canvasTriangleAnchors(pen) {
  const anchors = [];
  anchors.push({
    id: '0',
    penId: pen.id,
    x: 0.5,
    y: 0,
  });

  anchors.push({
    id: '1',
    penId: pen.id,
    x: 1,
    y: 1,
  });

  anchors.push({
    id: '2',
    penId: pen.id,
    x: 0,
    y: 1,
  });
  pen.anchors = anchors;
}

第二步,注册:

meta2d.registerCanvasDraw({canvasTriangle})  
//注册锚点  
meta2d.registerAnchors({canvasTriangle:canvasTriangleAnchors})

第三步,导入配置侧边栏:

async function getCanvasDraw() {
  const folderName = "canvasDraw/"
  let canvasDraw = []
  const canvasUrl = await userPensUrl.canvasDraw()
  for (let i of canvasUrl) {
    if (i.type === "directory") {
      const { data: files } = await axios.get(folderName + i.name + '/')
      let dataList = []
      for (let j of files) {
        const name = getFileName(j.name)
        dataList.push({
          name,
          icon: "t-icon t-" + canvasDrawMap[name], // 侧边栏展示图标  
          data: {
            width: 100,
            height: 100,
            name,
            text: name
          }
        })

      }
      canvasDraw.push({
        name: i.name,
        count: files.length,
        list: dataList,
        show: true,
      })
    }
  }
  return canvasDraw
}

因为与path2d的引入方式完全一样,所以这里不加以赘述了,直接来看效果。

导入canvasDraw 00_00_00-00_00_30.gif

到此,我们的各类自定义图标的引入就讲完了。

为了方便演示,在此文章中我使用的是乐吾乐提供的示例图元,在实际项目代码中该部分图元已经全部删除,用户可使用自己的图元文件来执行整个流程。

总结

本章,我们学习了如何导入各类各样的自定义图元库,为图元的扩充提供更多可能,其实,仅仅作为一个可视化流程图编辑器来说,根本用不到这么多的图元,在这里讲的详细点是因为笔者不希望读者把重心放在表面需求上,希望读者能够去深入了解meta2d,并用他创造更多可能应用在更广的地方。在下一章,我们继续回到setting组件部分的讲解,向大家介绍全局配置组件的实现。

Meta2d.js 开源地址

给大家推荐一下 Meta2d.js是一个实时数据响应和交互的2d引擎,可用于Web组态,物联网,数字孪生等场景。

Github:https://github.com/le5le-com/meta2d.js

Gitee: https://gitee.com/le5le/meta2d.js

如果本篇文章帮助到了你,欢迎为meta2d项目star点星。
nvasDraw
}


因为与path2d的引入方式完全一样,所以这里不加以赘述了,直接来看效果。

[外链图片转存中...(img-8icqbHb4-1694571235026)]

到此,我们的各类自定义图标的引入就讲完了。

> 为了方便演示,在此文章中我使用的是乐吾乐提供的示例图元,在实际项目代码中该部分图元已经全部删除,用户可使用自己的图元文件来执行整个流程。

## 总结

本章,我们学习了如何导入各类各样的自定义图元库,为图元的扩充提供更多可能,其实,仅仅作为一个可视化流程图编辑器来说,根本用不到这么多的图元,在这里讲的详细点是因为笔者不希望读者把重心放在表面需求上,希望读者能够去深入了解meta2d,并用他创造更多可能应用在更广的地方。在下一章,我们继续回到setting组件部分的讲解,向大家介绍全局配置组件的实现。

## Meta2d.js 开源地址

给大家推荐一下 Meta2d.js是一个实时数据响应和交互的2d引擎,可用于Web组态,物联网,数字孪生等场景。

Github:<https://github.com/le5le-com/meta2d.js>

Gitee: <https://gitee.com/le5le/meta2d.js>

如果本篇文章帮助到了你,欢迎为meta2d项目star点星。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值