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); // 这个是拿到gif (!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-ignorethis.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-ignoredom.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=resizeconst 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;// 如果自己大于等于最小值,则再减1if (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;// 如果自己大于等于最小值,则再减1if (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;// 解析当前 transformconst 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;// 解析当前 transformconst 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.mzph.cn/news/759849.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【网页实战项目设计】基于SSM的高校二手交易平台 (大学跳蚤市场)(源码+LW文档)

基于SSM的高校二手交易平台 &#xff08;大学跳蚤市场&#xff09; 项目截图 系统功能界面图 用户注册、用户登录界面图 个人中心界面图 商品信息界面图 商品上架管理界面图 订单信息管理界面图 开发环境与技术框架 开发语言&#xff1a;Java 框架&#…

SV-7035VP播放模块通用型播放终端SV-7035VP-SIP 网络通用型播放功放模块

SV-7035VP播放模块通用型播放终端SV-7035VP-SIP 网络通用型播放功放模块 产品介绍 SV-7035VP模块是一款SIP播放模块&#xff0c;具有10/100M以太网接口&#xff0c;其接收网络的音频数据&#xff0c;提供立体声的音频输出。 本SIP播放模块带有一个继电器端子和一个NET接口&a…

RabbitMQ-同步和异步区别快速入门

文章目录 1.1.同步和异步通讯1.1.1.同步通讯1.1.2.异步通讯 1.2.技术对比&#xff1a; 2.快速入门2.1.安装RabbitMQ2.2.RabbitMQ消息模型2.3.入门案例2.3.1.publisher实现 1.1.同步和异步通讯 服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&am…

Simulink如何自动保存模型,解决崩溃问题

文章目录 Simulink如何自动保存模型,解决崩溃问题自动保存模型脚本脚本使用方法专题目录 Simulink如何自动保存模型,解决崩溃问题 自动保存模型脚本 在使用sumilink建模中,暂无自动保存模型的相关设置,使用中常常会遇到软件卡死奔溃现象 当出现如上提示,如果花费了半天建…

在抖音电商迎上新趋势,他们找到春季服饰生意新增量

冬去春来&#xff0c;万物焕新。每年春天换季的时候&#xff0c;都是大家买新衣服的高峰期。 对于服饰商家来说&#xff0c;季节性的平台活动&#xff0c;能够带来较大的消费需求扩容空间&#xff0c;也是推动生意增长的好时机。在3月9日-3月14日举办的「抖音商城服饰焕新周」…

FreeCAD傻瓜教程之创建参数化几何图形-螺旋体、平面、球体、椭球体、圆柱体、圆锥体、棱柱、椭圆

目的&#xff1a;学会用FreeCAD绘制参数化的几何图形。 一、使用的工作台和工具 1.1选择Part 工作台 1.2单击创建图元...工具 也就是上图黄色工具区域的倒数第2个 1.3 打开几何图元 下方的下拉列表 二、绘制螺旋体、弹簧、螺丝杆 2.1 选择几何图元列表中的 “螺旋体” 设…

Mysql主从之keepalive+MySQL高可用

一、Keepalived概述 keepalived 是集群管理中保证集群高可用的一个服务软件&#xff0c;用来防止单点故障。 keepalived 是以VRRP 协议为实现基础的&#xff0c;VRRP 全称VirtualRouter Redundancy Protocol&#xff0c;即虚拟路由冗余协议。虚拟路由冗余协议&#xff0c;可以…

武汉星起航领航中国跨境电商,助推全球贸易新篇章

在全球经济一体化的浪潮中&#xff0c;跨境电商以其独特的优势成为推动国际贸易增长的重要引擎。作为中国跨境电商行业的领军者&#xff0c;武汉星起航电子商务有限公司以其卓越的能力和经验&#xff0c;正积极推动中国跨境电商走向世界&#xff0c;书写全球贸易的新篇章。 自…

leetcode 18.四数之和 java

题目 思路 整体在三数之和的基础上进行修改。&#xff08;所有需要修改的地方&#xff0c;我在代码里加了//改 的注释&#xff09; 大的一个思路就是&#xff0c;在三数之和的外面再套一层循环。相当于固定前两个数。然后这道题目标值变成一个参数了&#xff0c;不是三数之和…

termux安装

termux安装Python和postgres 安装python 安装pg数据库

语音神经科学—05. Human cortical encoding of pitch in tonal and non-tonal languages

Human cortical encoding of pitch in tonal and non-tonal languages&#xff08;在音调语音和非音调语言中人类大脑皮层的音高编码&#xff09; 专业术语 tonal language 音调语言 pitch 音高 lexical tone 词汇音调 anatomical properties 解刨学特性 temporal lobe 颞叶 s…

基于Java的桃花峪滑雪场租赁系统(Vue.js+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 游客服务2.2 雪场管理 三、数据库设计3.1 教练表3.2 教练聘请表3.3 押金规则表3.4 器材表3.5 滑雪场表3.7 售票表3.8 器材损坏表 四、系统展示五、核心代码5.1 查询教练5.2 教练聘请5.3 查询滑雪场5.4 滑雪场预定5.5 新…

算法打卡day22|回溯法篇02|Leetcode 216.组合总和III、17.电话号码的字母组合

算法题 Leetcode 216.组合总和III 题目链接:216.组合总和III 大佬视频讲解&#xff1a;组合总和III视频讲解 个人思路 在昨日做过的组合问题后&#xff0c;这道题的限制 多了两个&#xff1a;1.要找到和为n的k个数的组合&#xff0c;2.整个集合已经是固定的了[1,...,9]&…

用Python直接获取Word文档页数、字数、段落数、节数等信息

计算 Word 文档的页数、字数等信息是出版、学术和内容管理等领域的一项基本任务。准确的页数和字数对于评估文档长度、估算印刷成本、分析文本复杂性以及确保符合格式化指南至关重要。逐个预览文档查看相关信息是非常麻烦的事情&#xff0c;我们可以在不预览文档的情况下&#…

分享一篇Oracle RAC实战安装11G

分享一次很久以前的Oracle rac项目实施。 1、拓扑结构 基础环境是2台H3C的服务器2台3PAR的双活存储&#xff0c;操作系统centos7.2。借用下别人家的拓扑先&#xff08;这是一套典型的RAC架构&#xff09;。 2、网卡TEAM操作 以eno51和en052组成Team1组为示例&#xff1a; nm…

第十二届蓝桥杯大赛软件赛决赛C/C++ 研究生组-纯质数

直接判断数据过大 相对而言&#xff0c;由2&#xff0c;3&#xff0c;5&#xff0c;7组成的数更少&#xff0c;则先筛选出由2,3,5,7组成的数&#xff0c;再判断这些数中的质数个数即可 #include <iostream> using namespace std; int main() {printf("1903");…

3.7 RK3399项目开发实录-板载OpenWRT系统的使用(wulianjishu666)

STM32F103单片机从零到项目开发程序实例 下载链接&#xff1a;https://pan.baidu.com/s/1dWNskNinrMk4bxaE-jgHhQ?pwdymn3 1. OpenWRT 手册 1.1. 支持设备列表 主控板卡型号RK3568ROC-RK3568-PC/Station-P2 1.2. 登录 IP 、登录密码和 WIFI 名称 固件默认登录 IP 为 192.1…

化妆品行业电商数据分析

随着互联网经济的高速发展&#xff0c;电商已经成为化妆品经营的主要渠道之一。根据网络数据显示&#xff0c;11月全国化妆品网络活跃商品200万&#xff0c;零售总额为400亿。分区域来看&#xff0c;上海市、广东省、浙江省化妆品行业网络零售突出&#xff0c;三个区域网络零售…

2024.3.9|第十五届蓝桥杯模拟赛(第三期)

2024.3.9|十五届蓝桥杯模拟赛&#xff08;第三期&#xff09; 第一题 第二题 第三题 第四题 第五题 第六题 第七题 第八题 第九题 第十题 心有猛虎&#xff0c;细嗅蔷薇。你好朋友&#xff0c;这里是锅巴的C\C学习笔记&#xff0c;常言道&#xff0c;不积跬步无以至千里&…

webpack5零基础入门-10babel的使用

Babel JavaScript 编译器。 主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法&#xff0c;以便能够运行在当前和旧版本的浏览器或其他环境中 1.安装相关包 npm install -D babel-loader babel/core babel/preset-env 2.进行相关配置 2.1第一种写法是在webp…