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

系列文章

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

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

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

实现对齐辅助线

        在 logicFlow 中,辅助线的实现是通过遍历节点的位置信息计算得出,源码如下:

        在本项目中,节点的位置计算打算放置到 worker 中进行处理:

const worker = new Worker("/src/core/Worker/AuxiliaryLine.worker.ts");

// mousedown 中, 启用 worker 计算位置(放置move频繁计算导致页面卡顿)
public mousedown(e: MouseEvent, graph: IGraph) {
    this.allNode = this.draw.getAllNodeInfo();
}

         移动过程中,实时计算当前移动元素的位置,并利用 postMessage 给worker 传参,计算后,如果需要显示辅助线,则通过 onmessage 接收。然后在 worker中进行位置比较,如果达到辅助线的显示要求,则将显示辅助线的参数返回即可。

        我们先定义辅助线的几种场景(当然,垂直方向也是类似的哈 ):

        从左往右移动过程中,能显示辅助线的场景无非上诉几种,根源上,还是一个矩形,能显示辅助线的几种情况如下: 

        上图中,一个矩形一共有6条线需要参与计算,只需要得到6条线段的位置坐标,与 allData进行位置比较即可。

        关键代码如下:

self.onmessage = (event) => {
  const { current, allNode } = event.data;
  const list = allNode as INodeInfo[];
  const { v1, v2, v3, h1, h2, h3 } = computedLine(current);
  const varr = [v1, v2, v3];
  const harr = [h1, h2, h3];
  // 定义返回结果
  var result: { num: number; type: string }[] = [];
  // 循环
  list.forEach((node) => {
    if (node.ID === current.ID) return;
    const nodeLine = computedLine(node);

    if (varr.find((i) => i === nodeLine.v1))
      result.push({ num: nodeLine.v1, type: "v" });

    if (varr.find((i) => i === nodeLine.v2))
      result.push({ num: nodeLine.v2, type: "v" });

    if (varr.find((i) => i === nodeLine.v3))
      result.push({ num: nodeLine.v3, type: "v" });

    if (harr.find((i) => i === nodeLine.h1))
      result.push({ num: nodeLine.h1, type: "h" });

    if (harr.find((i) => i === nodeLine.h2))
      result.push({ num: nodeLine.h2, type: "h" });

    if (harr.find((i) => i === nodeLine.h3))
      result.push({ num: nodeLine.h3, type: "h" });
  });

  // 返回结果,确保每次移动只会返回一次结果,而不是循环返回多次,会导致某些线段无法渲染问题
  postMessage(result);
};

        可能需要在辅助线的位置进行吸附,就更加明显了,属于优化哈,后期慢慢处理。

 快捷键实现

document.addEventListener("keydown", this.globalKeydown.bind(this));

// 事件具体实现
for (let s = 0; s < eventList.length; s++) {
      const shortCut = eventList[s];

      if (
        (shortCut.mod
          ? isMod(evt) === !!shortCut.mod
          : evt.ctrlKey === !!shortCut.ctrl &&
            evt.metaKey === !!shortCut.meta) &&
        evt.shiftKey === !!shortCut.shift &&
        evt.altKey === !!shortCut.alt &&
        evt.key === shortCut.key
      ) {
        if (!shortCut.disable) {
          // 执行回调
          shortCut?.callback?.();
        }
        break;
      }
    }

        快捷键的关键代码就是给document添加 keydown 事件, 通过 for 遍历用户自定义的快捷键列表,比对 event.key 与用户的key 是否一致,进而调用 callback 实现。在此基础上,可以实现上下左右的 graph 移动事件:

  private graphMoveHandle(d: string, payload: cbParams | undefined) {
    const step = 10;
    const minstep = 2;
    // 1. 判断是否有选中的节点
    const selector = 'g[class="svg-flow-node svg-flow-node-selected"]';
    const g = this.rootSVG.querySelector(selector); // 这个是拿到g
    if (!g) return;
    // 2. 通过 g 拿到 实际的元素
    const element = g.querySelector('[type="graph"]') as SVGAElement;
    const nodeID = element.getAttribute("graphID");

    // 3. 通过 graphMap 获取 x y 属性
    const x = Number(element.getAttribute(graphMap[element.tagName][0]));
    const y = Number(element.getAttribute(graphMap[element.tagName][1]));
    const rd = payload?.ctrl ? minstep : step;

    // 3. 执行移动的实际逻辑
    if (d == "0")
      element.setAttribute(graphMap[element.tagName][0], (x - rd).toString()); // 左移
    if (d == "1")
      element.setAttribute(graphMap[element.tagName][1], (y - rd).toString()); // 左移
    if (d == "2")
      element.setAttribute(graphMap[element.tagName][0], (x + rd).toString()); // 左移
    if (d == "3")
      element.setAttribute(graphMap[element.tagName][1], (y + rd).toString()); // 左移

    // 4. 处理 形变、连接锚点位置
    const grapg = new Graph(this, element);
    this.createFormatAnchorPoint(element, grapg);
    this.updateLinkAnchorPoint(nodeID as string, element);
  }

 

框选选择

        通过给根元素添加 mouse 事件实现,框线开始时,需要设置 move、记录初始 sx sy 、显示 select-mask,移动过程中进行框选div的属性设置,移动结束后,记录结束位置。

this.rootDIV.addEventListener("mousedown", this._mouseDown.bind(this));
this.rootDIV.addEventListener("mousemove", this._mouseMove.bind(this));
this.rootDIV.addEventListener("mouseup", this._mouseUp.bind(this));

        关键代码如下:

  /**
   * 框选开始 - mouseDown
   *  设置 move、记录初始 sx sy 、显示 select-mask
   * @param e
   */
  private _mouseDown(e: MouseEvent) {
    this.move = true;
    this.rootSVG
      .querySelectorAll("g")
      // @ts-ignore
      .forEach((i) => (i.style["pointer-events"] = "none"));

    const selector = 'div[class="select-mask"]';
    this.maskdom = this.rootDIV.querySelector(selector) as HTMLDivElement;

    // @ts-ignore
    this.maskdom.style["pointer-events"] = "none";
    const { offsetX, offsetY } = e;
    this.sx = offsetX;
    this.sy = offsetY;
    this.maskdom.style.left = offsetX + "px";
    this.maskdom.style.top = offsetY + "px";
    this.maskdom.style.display = "block";
  }

  /**
   * 移动过程绘制框框
   * @param e
   */
  private _mouseMove(e: MouseEvent) {
    if (!this.move) return;
    const { offsetX, offsetY } = e;
    // 这里处理反向框选   x 往左边拖动,则拖动的位置始终是left的坐标,宽度则是计算的处
    if (offsetX - this.sx < 0) this.maskdom.style.left = `${offsetX}px`;

    if (offsetY - this.sy < 0) this.maskdom.style.top = `${offsetY}px`;

    this.maskdom.style.height = `${Math.abs(offsetY - this.sy)}px`;

    this.maskdom.style.width = `${Math.abs(offsetX - this.sx)}px`;
  }

  /**
   * 移动结束 记录结束位置,用于计算框选的宽高位置信息,以确定谁被选中
   * @param e
   */
  private _mouseUp(e: MouseEvent) {
    const selector = 'div[class="select-mask"]';
    const dom = this.rootDIV.querySelector(selector) as HTMLDivElement;
    this.move = false;
    // @ts-ignore 设置 svg 可响应
    this.rootSVG
      .querySelectorAll("g")
      // @ts-ignore
      .forEach((i) => (i.style["pointer-events"] = ""));

    // @ts-ignore
    dom.style["pointer-events"] = "";

    const { offsetX, offsetY } = e;
    // 记录抬起位置
    this.ex = offsetX;
    this.ey = offsetY;

    // 进行选中计算

    // 进行重置参数
    dom.style.display = "none";
    dom.style.left = "0";
    dom.style.top = "0";
    dom.style.width = "0";
    dom.style.height = "0";
  }

        结果处理中,需要根据 sx sy ex ey的位置信息,判断哪个元素被选中,添加 selected 样式即可:

  /**
   * 计算选中结果
   * @returns
   */
  private computedResult() {
    return new Promise<string[]>((resolve, reject) => {
      let x = [Math.min(this.sx, this.ex), Math.max(this.sx, this.ex)];
      let y = [Math.min(this.sy, this.ey), Math.max(this.sy, this.ey)];

      // 定义被选中的元素数组
      let selected: string[] = [];
      this.getAllNodeInfo().forEach(({ ID, cx, cy, w, h }) => {
        // 通过 cx cy w h 计算元素的 4 个角的坐标
        const lt = { x: cx - w / 2, y: cy - h / 2 };
        const rt = { x: cx + w / 2, y: cy - h / 2 };
        const lb = { x: cx - w / 2, y: cy + h / 2 };
        const rb = { x: cx + w / 2, y: cy + h / 2 };

        // 判断 4 个角是否处于框选范围内
        const islt = this.computedIsSelected(lt, x, y);
        const isrt = this.computedIsSelected(rt, x, y);
        const islb = this.computedIsSelected(lb, x, y);
        const isrb = this.computedIsSelected(rb, x, y);

        function inside() {
          if (islt || isrt || isrb || islb) selected.push(ID);
        }

        function all() {
          if (islt && isrt && isrb && islb) selected.push(ID);
        }

        this.mode === "inside" ? inside() : all();
      });

      resolve(selected);
    });
  }

插件化

        插件化指的是通过 plugin 实现拓展功能,例如元件库、顶部操作区,底部显示等:

        当然,插件化的所有功能实现,均需有对应的API实现,不然用户不加载你的插件,连基础的功能都实现不了,插件化的核心就是脱离页面,可通过API调用实现响应功能。

         定义 footer 模板,添加样式,实现加载:

        实现缩放,缩放的核心是 scale 实现:

/**
   * 实现缩放的关键方法 单独出来是为了供 command 实现调用
   * @param scale
   */
  public scalePage(scale: number) {
    const editorBox = this.draw.getEditorBox() as HTMLDivElement;
    // 考虑临界值  实现缩放
    editorBox.style.transform = `scale(${scale})`;
    // 同时还需要考虑 footer 的缩放比例同步显示
    const root = this.draw.getRoot();
    const footerBox = root.querySelector('[class="sf-editor-footer"]');
    if (footerBox) {
      // 修改缩放比例 command=resize
      const resize = footerBox.querySelector(
        '[command="resize"]'
      ) as HTMLSpanElement;
      resize.innerHTML = Math.ceil(scale * 100).toString() + "%";
    }
    // 执行 pageScale 回调
    nextTick(() => {
      const eventBus = this.draw.getEventBus();
      const listener = this.draw.getListener();
      const graphLoadedSubscribe = eventBus.isSubscribe("pageScale");
      graphLoadedSubscribe && eventBus.emit("pageScale", scale);
      listener.pageScale && listener.pageScale(scale);
    });
  }

        上图是加载了所有插件的样式,包括顶部操作区,左侧元件库,底部信息展示。 

旋转实现

.rotate {
    // background-color: red;
    background: url('/public/rotate.svg') 100% 100% no-repeat;
    position: absolute;
    right: -10px;
    top: -10px;
    height: 16px;
    width: 16px;
    cursor: url('/public/rotate.svg'), auto;
}

        通过 cursor url 指定一个svg ,可以实现hover后鼠标样式的修改:

        通过鼠标的位置计算出旋转角度:

    // 执行旋转的关键函数
    function rotateHandle(e: MouseEvent) {
      // 需要通过计算得出旋转的角度
      const centerX = x + width / 2;
      const centerY = y + height / 2;
      const mouseX = e.offsetX;
      const mouseY = e.offsetY;
      const deltaX = mouseX - centerX;
      const deltaY = mouseY - centerY;
      let angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
      graph.setRotate(angle - 136 + 180); // 加减是为了抵消默认旋转角度的影响
    }

         但是目前旋转后,对拖动、缩放都有影响,因为旋转后的位置坐标相对的 e 事件,导致了

offset 位置变化。大家有什么实现思路,可以讨论下

层级处理 

        基于 div 的zIndex 实现层级:

 // 置于顶层
  public top() {
    const isSelected = this.draw.getGraphEvent().getSelected();
    if (!isSelected) return;
    const allSelected = this.draw.getGraphEvent().getAllGraphMain();

    var zIndexArr: number[] = [];
    allSelected.forEach((div) => zIndexArr.push(~~div.style.zIndex));
    const max = Math.max.apply(Math, zIndexArr);
    const index = ~~isSelected.style.zIndex;
    // 如果自己大于等于最小值,则再减1
    if (index <= max)
      isSelected.style.zIndex =
        index === 1 ? index.toString() : (index + 2).toString();
  }

  // 置于底层
  public bottom() {
    const isSelected = this.draw.getGraphEvent().getSelected();
    if (!isSelected) return;
    const allSelected = this.draw.getGraphEvent().getAllGraphMain();
    var zIndexArr: number[] = [];
    allSelected.forEach((div) => zIndexArr.push(~~div.style.zIndex));
    // 找到数组中最小的
    const min = Math.min.apply(Math, zIndexArr);
    const index = ~~isSelected.style.zIndex;
    // 如果自己大于等于最小值,则再减1
    if (index >= min)
      isSelected.style.zIndex =
        index === 1 ? index.toString() : (index - 2).toString();
  }

  // 上移一层
  public holdup() {
    const isSelected = this.draw.getGraphEvent().getSelected();
    if (!isSelected) return;
    // 获取当前的层级 进行++
    const index = ~~isSelected.style.zIndex;
    isSelected.style.zIndex = (index + 1).toString();
  }

  // 下移一层
  public putdown() {
    const isSelected = this.draw.getGraphEvent().getSelected();
    if (!isSelected) return;
    // 获取当前的层级 进行--
    const index = ~~isSelected.style.zIndex;
    // 不能是 -1 不然就选不到了
    isSelected.style.zIndex =
      index === 1 ? index.toString() : (index - 1).toString();
  }

空格平移

        通过监听 keydown 识别space,监听 mousedown、mousemove、mouseup的事件,利用transform属性实现平移,关键代码如下:

  /**
   * 空格左键单击记录初始位置
   */
  private spaceDown(e: MouseEvent) {
    if (e.buttons === 2) return;
    this.move = true;
    this.sx = e.offsetX;
    this.sy = e.offsetY;
    // 解析当前 transform
    const editorBox = this.draw.getEditorBox();
    const transform = editorBox.style.transform.split(" "); // ['scale(1)', 'translate(0px,', '0px)']

    // 解析当前的偏移量
    this.tx = Number(transform[1].replace(/translate\(|px|,/g, ""));
    this.ty = Number(transform[2].replace(/\)|px/g, ""));
  }

  /**
   * 空格移动位置
   */
  private spaceMove(e: MouseEvent) {
    if (!this.move) return;
    const { offsetX, offsetY } = e;
    const dx = offsetX - this.sx;
    const dy = offsetY - this.sy;

    // 解析当前 transform
    const editorBox = this.draw.getEditorBox();
    const transform = editorBox.style.transform.split(" "); // ['scale(1)', 'translate(0px,', '0px)']

    // 计算最终结果
    const result = `translate(${this.tx + dx}px, ${this.ty + dy}px)`;
    editorBox.style.transform = transform[0] + result;
  }

 总结

        本篇历时较久,原因是对项目进行了重构,不再使用单一 svg 实现整个元件的实现,而是使用div+svg的结构实现,使得旋转、层级处理上更加简单;目前旋转后,会导致一些位置异常问题,还有待深究,大家有好的想法,欢迎留言讨论呀,同时发布npm后,worker路径也会有问题,个人能力有限,如果大家有好的解决办法,可以分享下。下一篇的重点是实现折线的绘制,以及command API的完善。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/476616.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloudGateway之高性能篇

SpringCloudGateway之高性能篇 背景 在公司的开放平台中&#xff0c;为了统一管理对外提供的接口、处理公共逻辑、实现安全防护及流量控制&#xff0c;确实需要一个API网关作为中间层。 场景 统一接入点: API网关作为所有对外服务的单一入口&#xff0c;简化客户端对内部系统…

基于python+vue渔船出海及海货统计系统的设计与实现flask-django-php-nodejs

当今社会已经步入了科学技术进步和经济社会快速发展的新时期&#xff0c;国际信息和学术交流也不断加强&#xff0c;计算机技术对经济社会发展和人民生活改善的影响也日益突出&#xff0c;人类的生存和思考方式也产生了变化。传统渔船出海及海货统计采取了人工的管理方法&#…

2024.3.22 使用nginx在window下运行前端页面

2024.3.22 使用nginx在window下运行前端页面 使用nginx可以在本地运行前端程序&#xff0c;解决本地前后端程序跨域问题&#xff0c;是个前期编程及测试的好办法。 nginx下载 直接在官网下载 本次选择了1.24版本&#xff08;stable version&#xff09; nginx安装 解压后…

低压MOS在无人机上的应用-REASUNOS瑞森半导体

一、前言 无人机的结构由机身、动力系统、飞行控制系统、链路系统、任务载荷等几个方面组成的。 无人机动力系统中的电机&#xff0c;俗称“马达”&#xff0c;是无人机的动力来源&#xff0c;无人机通过改变电机的转速来改变无人机的飞行状态。即改变每个电机的速度&#xf…

vue+element 前端实现增删查改+分页,不调用后端

前端实现增删查改分页&#xff0c;不调用后端。 大概就是对数组内的数据进行增删查改分页 没调什么样式&#xff0c;不想写后端&#xff0c;当做练习 <template><div><!-- 查询 --><el-form :inline"true" :model"formQuery">&l…

牛客NC403 编辑距离为一【中等 模拟法 Java,Go,PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/0b4b22ae020247ba8ac086674f1bd2bc 思路 注意&#xff1a;必须要新增一个&#xff0c;或者删除一个&#xff0c;或者替换一个&#xff0c;所以不能相等1.如果s和t相等&#xff0c;返回false,如果s和t长度差大于1…

全栈的自我修养 ———— uniapp中加密方法

直接按部就班一步一步来 一、首先创建一个js文件填入AES二、创建加密解密方法三、测试 一、首先创建一个js文件填入AES 直接复制以下内容 /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wi…

HTML_CSS学习:表格、表单、框架标签

一、表格_跨行与跨列 1.相关代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>表格_跨行与跨列</title> </head> <body><table border"1" cellspacing"0&qu…

5 线程网格、线程块以及线程(1)

5.1 简介 英伟达为它的硬件调度方式选择了一种比较有趣的模型&#xff0c;即SPMD(单程序多数据Single Program&#xff0c;Multiple Data)&#xff0c;属于SIMD(单指令多数据)的一种变体。从某些方面来说&#xff0c;这种调度方式的选择是基于英伟达自身底层硬件的实现。并行编…

GPT-5可能会在今年夏天作为对ChatGPT的“实质性改进”而到来

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

代码随想录算法训练营第十六天|104.二叉树的最大深度、559.n叉树的最大深度、111.二叉树的最小深度、222.完全二叉树的节点个数

代码随想录算法训练营第十六天|104.二叉树的最大深度、559.n叉树的最大深度、111.二叉树的最小深度、222.完全二叉树的节点个数 104.二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数…

为什么物联网网关需要边缘计算能力?边缘计算应用场景有哪些?

【前言】本篇为物联网硬件系列学习笔记&#xff0c;分享学习&#xff0c;欢迎评论区交流~ 什么是边缘计算&#xff1f; 边缘计算&#xff08;Edge Computing&#xff09;是一种分布式计算范式&#xff0c;旨在将计算和数据存储功能放置在接近数据源或终端设备的边缘位置&#…

OSError: We couldn‘t connect to ‘https://huggingface.co‘ to load this file

想折腾bert的同学&#xff0c;应该也遇到这个问题。 一、报错信息分析 完整报错信息&#xff1a;OSError: We couldnt connect to https://huggingface.co to load this file, couldnt find it in the cached files and it looks like google/mt5-small is not the path to a…

扶贫惠农推介系统|基于jsp技术+ Mysql+Java+ B/S结构的扶贫惠农推介系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

GPT-5揭秘:Lex Fridman与Sam Altman播客热议,AGI时代的新变革即将来临!

嘿&#xff0c;朋友们&#xff0c;你们知道吗&#xff1f;Lex Fridman和Sam Altman又聚在一起了&#xff0c;这次是在播客上。 在播客中&#xff0c;他们聊了很多&#xff0c;包括董事会的幕后故事、Elon Musk的诉讼案&#xff0c;甚至还提到了Ilya、Sora这些名字。 但真正让…

3.21作业

1、使用sqlite3实现简易的学生管理系统 #include<myhead.h>//封装添加学生信息函数 int do_add(sqlite3 *ppDb) {//准备sql语句int add_numb 0;char add_name[20] "";double add_socre 0;printf("输入学号:");scanf("%d",&add_num…

React 系列 之 React Hooks(一) JSX本质、理解Hooks

借鉴自极客时间《React Hooks 核心原理与实战》 JSX语法的本质 可以认为JSX是一种语法糖&#xff0c;允许将html和js代码进行结合。 JSX文件会通过babel编译成js文件 下面有一段JSX代码&#xff0c;实现了一个Counter组件 import React from "react";export defau…

Java代码基础算法练习-求一个三位数的各位平方之和-2024.03.21

任务描述&#xff1a; 输入一个正整数n&#xff08;取值范围&#xff1a;100<n<1000&#xff09;&#xff0c;然后输出每位数字的平方和。 任务要求&#xff1a; 代码示例&#xff1a; package march0317_0331;import java.util.Scanner;public class m240321 {public …

java.lang.String final

关于String不可变的问题&#xff1a;从毕业面试到现在&#xff0c;一个群里讨论的东西&#xff0c;反正码农面试啥都有&#xff0c;这也是我不咋喜欢面试代码&#xff0c;因为对于我而言&#xff0c;我并不喜欢这些面试。知道或不知道基本没啥含氧量&#xff0c;就是看看源代码…

程序员为什么宁愿失业都不愿意搭伙去接单?

我要说一个关键词&#xff1a;信息差&#xff0c;这个词将贯彻这整篇文章。 大多数程序员觉得&#xff0c;接单不稳定、金额低、单子少&#xff0c;又卷又没有性价比。从某种角度上说&#xff0c;这个说法也没错&#xff0c;但作为业余接单者&#xff0c;本就不应该把接单当做主…
最新文章