Svg Flow Editor 原生svg流程图编辑器(四)

系列文章

Svg Flow Editor 原生svg流程图编辑器(一)

Svg Flow Editor 原生svg流程图编辑器(二)

Svg Flow Editor 原生svg流程图编辑器(三)

Svg Flow Editor 原生svg流程图编辑器(四)

实现Echart统计图

统计图的底层我们采用apache echarts 实现【Apache ECharts】,先封装GEchart,GEchart是我们的外层框架,支持形变、旋转,与Rect等svg元件类似的结构,但是内部是div实现:

同时,样式设置上与svg还是有些不同,需要单独处理下。

javascript 复制代码
  /**
   * 需要向外暴露 setOption 方法,供数据变化后重新渲染
   * @param option
   */
  private setOption() {
    if (!this.option) throw new Error(messageInfo.optionError);
    this.myChart.setOption(this.option);
    return this;
  }

  /**
   * 向外提供 update 方法,供用户在 option 变化后更新页面内容
   *  因 option 是引用地址,因此 可以不需要传递参数,从而实现数据更新
   * @returns
   */
  public update() {
    return this.setOption();
  }

事件上,则是核心的 setOption 与 update 两个方法,update则是向外提供给用户更新时使用。同时,缩放会导致父节点尺寸变化,因此还需要监听尺寸变化实现动态Echart重绘,使用第三方库实现此功能【也可以使用 const ob = new ResizeObserver() 这个原生API实现哈,看自己的需求】:

javascript 复制代码
import elementResizeDetectorMaker from "element-resize-detector";
var erd = elementResizeDetectorMaker();

// 监听元素尺寸变化,重新渲染echart 使得宽高自适应
erd.listenTo(this.div, () => this.myChart.resize());

还需要封装一层插件,因为GEchart是核心类,不能直接提供给用户,也不便于结构管理:

javascript 复制代码
// echarts 插件 多一层的原因是构建新的实例
export class SEchart {
  private draw: Draw;

  constructor(draw: Draw) {
    this.draw = draw;
  }

  /**
   * 初始化 Echart
   * @param option
   * @returns
   */
  public init(option: object) {
    return new GEchart(this.draw, option);
  }
}
javascript 复制代码
// 关键!需要注册插件,提供 echart 绘制能力  
const echart = editor.plugin("echart");

// 初始化echart
const line = echart?.init(option); 

// 模拟数据更新
setTimeout(() => {
    data[0] = "123123mode";
    line?.update();
}, 1000);

注意哈,class GEchart extends GraphCommon, GEchart 类继承了 Common 类,拥有元件的所有属性方法,包括 setWidth position 等;而 SEchart 则是隔离用户触碰核心类,同时也给用户注册多实例提供可能,也是对插件化提供支持。

Echart 的事件处理,则是基于EventBus 实现实例的 emit on 操作:

javascript 复制代码
this.event = new EventBus();

// Echarts click
this.myChart.on("click", (params: object) =>
    this.event.emit("click", params)
);

// Echarts 鼠标移出
this.myChart.on("mouseout", (params: object) =>
    this.event.emit("mouseout", params)
);

// Echarts 鼠标移入
this.myChart.on("mouseover", (params: object) =>
    this.event.emit("mouseover", params)
);

const echart = editor.plugin('echart')

const bar=echart.init(barOption)

bar.event.on('click',p=>{
    // p 是回调的参数
})

直角折线

以下实现思路参考logicFlow直角折线思想,连接锚点mousedown事件中,我们要记录当前锚点的位置信息:

javascript 复制代码
    // 1. 获取当前元素的宽高位置信息
    const width = graph.getWidth();
    const height = graph.getHeight();
    const x = graph.getX();
    const y = graph.getY();

    // 2. 需要计算起点位置 -- 锚点位置受padding影响
    const typeMap: { [key: string]: { sx: number; sy: number } } = {
      "0": { sx: x, sy: y + height / 2 },
      "1": { sx: x + width / 2, sy: y },
      "2": { sx: x + width, sy: y + height / 2 },
      "3": { sx: x + width / 2, sy: y + height },
    };
    const sx = typeMap[type].sx + 10;
    const sy = typeMap[type].sy + 10;

使用editorBox来接收mousemove事件以实现流程的拖动,但是在拖动过程中,可能会经过多个元素,导致 offset 值得变化,需要做优化:

javascript 复制代码
/**
 * 需要做位置矫正
 *  class='sf-editor-box-graphs' 正确
 *  class='sf-editor-box-graphs-main' 异常 偏移量加 offsetLeft offsetTop 值
 *  class='' 异常
*/

绘制结束后,需要根据logicFlow的思想,构建出下面这个图形:

实现的思路是根据线的起点、终点关联的元件计算得出:

实现关键代码:

javascript 复制代码
 console.log("### 绘制最终折线,根据框的宽高位置信息获取基础数据");
    const eid = this.line.getAttribute("eid") as string;
    const sid = this.line.getAttribute("sid") as string;
    if (!eid) return this.lineBox.remove();

    // 不然处理折线的寻径算法
    this.line.setAttribute("stroke-dasharray", "");

    // 1. 获取 sid eid 构建 graph
    const Sgraph = new Graph(this.draw, sid);
    const Egraph = new Graph(this.draw, eid);

    // 2. 获取start的宽高 位置信息
    const sx = Sgraph.getX();
    const sy = Sgraph.getY();

    // 3. 获取 end 的宽高 位置信息
    const ex = Egraph.getX();
    const ey = Egraph.getY();

    // 4. 需要知道哪个元件在最后 也就是 graph x 最大
    const maxGrapg = sx > ex ? Sgraph : Egraph;

    // 5. 构建 OFFSET 的矩形 --- 受padding的影响
    const lx = Math.min(sx, ex) - OFFSET + 10;
    const ly = Math.min(sy, ey) - OFFSET + 10;
    this.setX(lx);
    this.setY(ly);
    this.lineBox.style.backgroundColor = "rgba(0,0,0,0.1)";
    // 6. 取消直线
    this.line.setAttribute("points", "");

    // 7. 设置宽高
    const mw = maxGrapg.getWidth();
    const mh = maxGrapg.getHeight();
    const mx = maxGrapg.getX();
    const my = maxGrapg.getY();

    // 自此,整个线的宽高= lx -> mx + mw + OFFSET
    const lw = mx - lx + mw + OFFSET + 10;
    const lh = my - ly + mh + OFFSET + 10;
    this.setWidth(lw);
    this.setHeight(lh);

根据矩形框,找出所有可能路径关键点:

javascript 复制代码
    // 做点的纠正-因为 计算得到的是 基于背景的 而线的绘制基于新的 div 坐标,需要做处理 【并且受 padding 的影响】
    const getX = (x: number) => x - lx + 10;
    const getY = (y: number) => y - ly + 10;

    // 起点
    const startType = typeMap[st]({ x: sx, y: sy, w: sw, h: sh });
    const startPoint = { x: getX(startType.x), y: getY(startType.y) };
    points.push(startPoint);

    // 终点
    const endType = typeMap[et]({ x: ex, y: ey, w: ew, h: eh });
    const endPoint = { x: getX(endType.x), y: getY(endType.y) };
    points.push(endPoint);

    // 如果存在间隙,则取偏移量的点,如果不存在间隙,则不取偏移量的点
    const intervalX = maxGrapg.getX() - minGraph.getX() + minGraph.getWidth();
    const intervalY = maxGrapg.getY() - minGraph.getY() + minGraph.getHeight();
    if (intervalX > OFFSET && intervalY > OFFSET) {
      // 取偏移量
      const startOffsetPoint = { x: getX(startType.ox), y: getY(startType.oy) };
      points.push(startOffsetPoint);

      const endOffsetPoint = { x: getX(endType.ox), y: getY(endType.oy) };
      points.push(endOffsetPoint);
    }

轴线与边界的交点:

A* 算法实现:

javascript 复制代码
    const list: expandP[] = JSON.parse(JSON.stringify(points)); // 深拷贝简单实现
    var optimal: expandP[] = []; // 记录最优解

    // 计算当前点的最短路径
    const computedDistance = (p: expandP) => {
      console.group("开始 A* 算法");
      console.log("当前点", p);
      // 获取list中 x、y 相同的点,并计算最短路径
      const ps = list.filter((i: expandP) => i.x === p.x || i.y === p.y);
      // 循环当前可达的点,并计算距离
      ps.forEach((i: expandP) => {
        i.d = Infinity; // 默认无穷大
        // 并且计算传入的点与当前点的向量是否穿过矩形
        // 计算曼哈顿距离
        i.d = Math.abs(i.x - endPoint.x) + Math.abs(i.y - endPoint.y);
      });
      ps.sort((a, b) => (a.d as number) - (b.d as number));
      console.log("当前可达的点", ps);
      this.drawPoint(ps[0], "green");
      console.groupEnd();
      return ps[0];
    };

    /**
     * 【开始寻径算法】
     *  1. 找到当前点的 x y 相同的点作为可达点,
     *  2. 并且规定,当前点的可达距离为1,取到终点的曼哈顿距离
     *  3. 还需要判断当前两点的向量是否穿过矩形
     */
    const search = () => {
      const point = optimal.length ? optimal[optimal.length - 1] : startPoint; // 当前的最优解
      if (point.x === endPoint.x && point.y === endPoint.y)
        return console.log("A* 算法结束,最优路径为 => ", optimal);
      const optimalPoint = computedDistance(point);
      optimal.push(optimalPoint);
    };

这样会穿过元件,不符合,因此需要使用处理计算,判断是否穿过节点本身:

javascript 复制代码
  // 检查两个点组成的线段是否穿过起终点元素
  private checkLineThroughElements(p1: p, p2: p) {
    let rects = [this.Sgraph, this.Egraph];
    let minX = Math.min(p1.x, p2.x);
    let maxX = Math.max(p1.x, p2.x);
    let minY = Math.min(p1.y, p2.y);
    let maxY = Math.max(p1.y, p2.y);

    // 水平线
    if (p1.y === p2.y) {
      for (let i = 0; i < rects.length; i++) {
        let rect = rects[i];
        if (
          minY > rect.getY() - this.ly &&
          minY < rect.getY() + rect.getHeight() - this.ly &&
          minX < rect.getX() + rect.getWidth() - this.lx &&
          maxX > rect.getX() - this.lx
        ) {
          return true;
        }
      }
    } else if (p1.x === p2.x) {
      // 垂直线
      for (let i = 0; i < rects.length; i++) {
        let rect = rects[i];
        if (
          minX > rect.getX() - this.lx &&
          minX < rect.getX() + rect.getWidth() - this.lx &&
          minY < rect.getY() + rect.getHeight() - this.ly &&
          maxY > rect.getY() - this.ly
        ) {
          return true;
        }
      }
    }

    return false;
  }

推荐大家看一下该博客,我也借鉴了些思路,在点的分析,定位,算法的实现上,都看了他的代码,但是还有些部分需要根据项目实际进行优化的点。关联线探究,如何连接流程图的两个节点

最终实现效果:

这部分应该是最难的了,目前实现起来,在临界值的处理上,还是有些问题,包括距离相近时,和优化线的方向问题,还没有做兼容处理。

发布NPM

包的发布过程就不细说了,大家可以参考网上的教程,如果 npm login 报错,基本上切换淘宝镜像就可以解决问题了。目前经过 mvp 版本的升级迭代,基本具备流程图的绘制、交互功能,我们发布 1.0 版本,主要是测试应用功能模块是否正常。【worker 路径存在问题,目前版本使用同步实现,后续优化】

当你升级版本的时候报错,一定需要全部提交代码,才可以进行升级:

版本升级成功,可以在 npm 官网进行查看:

新建空项目,进行测试:

使用过程中,发现有些静态资源请求地址有问题,我们采取base64编码的方式处理,不走请求,即可解决该问题,整体效果如下:

总结

目前已经基本实现流程图的图形绘制、自定义icon 、文本输入,剩余的优化问题包括:

  1. 折线算法优化;

  2. 元件库拓展;

  3. 顶部菜单栏及相应 command API开发;

  4. 快捷键的完善与相应功能实现。

大家可以下载包试试,有啥问题随时反馈改进哦。

npm i svg-flow-editor-mvp

相关推荐
Mortal_hhh5 小时前
VScode的C/C++点击转到定义,不是跳转定义而是跳转声明怎么办?(内附详细做法)
ide·vscode·stm32·编辑器
小阮的学习笔记10 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
电子云与长程纠缠15 小时前
UE5.3中通过编辑器工具创建大纲菜单文件夹
java·ue5·编辑器
lucky九年15 小时前
vscode翻译插件
ide·vscode·编辑器
真·Wild·攻城狮16 小时前
【码农日常】Vscode Clangd初始化失败(Win10)
ide·vscode·编辑器
七灵微16 小时前
【测试】【Debug】vscode中同一个测试用例出现重复
ide·vscode·编辑器
4U2471 天前
Linux入门之vim
linux·编辑器·vim·命令模式·底行模式
Liquor14191 天前
vim 编辑器
java·linux·c语言·开发语言·python·编辑器·vim
skywalk81631 天前
三周精通FastAPI:33 在编辑器中调试
python·编辑器·fastapi
188_djh2 天前
# vim那些事...... vim删除文件全部内容
linux·ubuntu·centos·编辑器·vim·vi·vim删除文件全部内容