尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LogicFlow NodeResize 扩展深度解析:节点缩放实现原理与自定义指南

LogicFlow NodeResize 扩展深度解析:节点缩放实现原理与自定义指南 LogicFlow NodeResize 扩展深度解析节点缩放实现原理与自定义指南【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow导读本文以 LogicFlow 仓库中的 NodeResize 扩展实现文档 为核心结合logicflow/extension包中 NodeResize 插件的完整源码实现文档、使用教程与测试用例系统讲解节点缩放功能的设计思路、底层算法与配置方式。读完本文你将掌握节点缩放插件如何通过自定义节点机制工作、矩形/椭圆/菱形三种图形的缩放数学原理、与之相连的边如何随缩放自动重算端点以及如何通过继承RectResize/EllipseResize/DiamondResize快速实现支持缩放的业务自定义节点。一、功能背景为什么需要节点缩放LogicFlow 内置了矩形rectangle、圆形circle、菱形diamond、多边形polygon、椭圆ellipse、文本text等基础节点类型。业务系统往往需要让用户像操作画图软件一样通过拖拽节点边缘的控制点来实时调整节点大小——例如工作流中的审批框、流程节点需要被拉伸放大以容纳更多内容。LogicFlow 的节点缩放能力正是通过自定义节点机制实现的不修改核心引擎而是在扩展包中提供可缩放的增强型基础节点开发者通过继承它们即可获得缩放能力。1.1 支持的节点类型当前 NodeResize 支持三种图形矩形Rectangle椭圆Ellipse菱形Diamond为什么只支持这三种流程图中最常用的节点形状就是矩形、圆形用椭圆代替和菱形因此插件聚焦覆盖这三类高频场景。若业务需要其他形状可参照本文介绍的控制点绘制与缩放算法思路自行实现自定义缩放节点。为什么是椭圆而不是圆形椭圆在 rx 与 ry 相等时即为正圆而一个正圆被拖拽缩放后天然会变成椭圆。因此直接实现椭圆缩放即可同时覆盖圆形场景避免为圆形单独维护一套逻辑。1.2 插件的注册与定位在源码 NodeResize/index.ts 中插件被注册为nodeResizeinstall阶段一次性注册四种可缩放节点export const NodeResize { pluginName: nodeResize, step: 0, install(lf: LogicFlow) { lf.register(EllipseResize) lf.register(DiamondResize) lf.register(HtmlResize) lf.register(RectResize) }, }值得注意的是插件入口文件头部有一处明确的弃用说明index.ts该插件标注为deprecated2.0 版本将 NodeResize 能力内置到核心此插件设计与实现存在较多历史问题后续不再维护。因此本文既可作为理解当前版本实现原理的参考也可作为迁移到 2.0 内置能力前的知识铺垫。插件通过 packages/extension/src/index.ts 的export * from ./NodeResize对外导出。二、缩放效果与交互形态缩放交互表现为节点被选中时其边框四角出现 4 个可拖拽的控制点方形小方块拖拽控制点即实时改变节点大小。四个控制点的位置与顺序在 ControlGroup.tsx 中定义顺序固定为左上角index 0、右上角index 1、右下角index 2、左下角index 3这一顺序直接决定了后续缩放增量计算的方向。const controlList [ { x: minX, y: minY }, // 0 左上角 { x: maxX, y: minY }, // 1 右上角 { x: maxX, y: maxY }, // 2 右下角 { x: minX, y: maxY }, // 3 左下角 ]三、实现思路继承 覆盖 getShape3.1 缩放操作的核心机制缩放节点通过**继承基础节点类型并重写节点绘制方法getShape**来实现在原图形之上追加 4 个控制点拖拽控制点即触发缩放。控制点的实现与logicflow/core保持一致使用Preact绘制如 Control.tsx 中通过h()与Rect渲染控制点方块。以矩形为例RectResize.tsx 中的视图层结构如下export class RectResizeView extends RectNode { getControlGroup(): h.JSX.Element | null { const { model, graphModel } this.props return ControlGroup model{model} graphModel{graphModel} / } // getResizeShape 绘制图形功能等同于基础矩形的 getShape getResizeShape() { return super.getShape() } getShape(): h.JSX.Element { return g{this.getResizeShape()}/g } }这里引入了一个对自定义节点至关重要的约定下文自定义节点章节会详述自定义可缩放节点的外观应重写getResizeShape而非getShape因为getShape已经被基类占用来组装节点图形 控制点组。3.2 拖拽控制点后的四方面调整每次拖拽控制点需要同步调整四类数据节点位置x, y节点文本位置textPosition节点尺寸矩形为 width/height椭圆与菱形为 rx/ry相连边的路径与端点pointsListLogicFlow 采用 MVVM 模式绘制视图层的调整最终都落回到模型层的属性更新上。拖拽的完整调度链位于 Control.tsx 的onDragging中——它先用transformModel.fixDeltaXY修正画布缩放/平移带来的位移增量再按modelType分发到updateRect/updateEllipse/updateDiamond三个更新方法HTML 节点与矩形共用updateRect。节点位置与文本位置基于控制点被拖拽的位移节点中心坐标与文本位置各移动一半距离updatePosition ({ deltaX, deltaY }) { const { x, y } this.nodeModel this.nodeModel.x x deltaX / 2 this.nodeModel.y y deltaY / 2 this.nodeModel.moveText(deltaX / 2, deltaY / 2) }节点尺寸基于控制点拖拽位移按比例增减节点的宽高。矩形直接调整 width/height椭圆与菱形调整 rx/rywidth/height 为计算属性自动随之更新。增量计算的关键参数index控制点顺序索引依次为左上、右上、右下、左下deltaX / deltaY控制点本次拖拽的位移pct计算 width、height、rx、ry 时使用的系数——矩形为 1椭圆与菱形为 1/2因为后者的 rx/ry 是半轴长度缩放半轴需要折半。核心算法如下Control.tsx 中getResize的简化版与文档一致getResize ({ index, deltaX, deltaY, width, height, pct 1 }) { const resize { width, height } switch (index) { case 0: // 左上角向左上拖动则变小 resize.width width - deltaX * pct resize.height height - deltaY * pct break case 1: // 右上角向右上拖动宽度增、高度减 resize.width width deltaX * pct resize.height height - deltaY * pct break case 2: // 右下角向右下拖动则变大 resize.width width deltaX resize.height height deltaY * pct break case 3: // 左下角向左下拖动宽度减、高度增 resize.width width - deltaX * pct resize.height height deltaY * pct break default: break } return resize }源码中该方法的实现比文档示例更完整额外支持freezeWidth/freezeHeight当minWidth maxWidth或minHeight maxHeight时锁定宽或高对应锁定状态下节点中心不再产生位移、PCTResizeInfo等比缩放模式等扩展能力。得到 resize 结果后按类型更新数据矩形width resize.widthheight resize.height椭圆rx resize.widthry resize.height菱形rx resize.widthry resize.height以椭圆更新为例Control.tsx更新 rx/ry 后通过setProperties({ nodeSize: { rx, ry } })将尺寸写回节点属性便于序列化与回显。调整相连边与路径节点位置与尺寸变化后与之相连的边必须随之更新。从节点出发的边sourceEdges只需更新起点路径即自动重算进入节点的边targetEdges更新终点即可。文档中以矩形为例let afterPoint const edges this.getNodeEdges(id) edges.sourceEdges.forEach((item) { params.point item.startPoint afterPoint getRectReizeEdgePoint(params) item.updateStartPoint(afterPoint) }) edges.targetEdges.forEach((item) { params.point item.endPoint afterPoint getRectReizeEdgePoint(params) item.updateEndPoint(afterPoint) })需要说明的是当前源码已演进为updateEdgePointByAnchors()方案见 Control.tsx直接读取节点自定义锚点getDefaultAnchor()计算出的最新锚点坐标作为边的新端点从而解决用户自定义锚点如x: x 20的固定偏移写法无法跟随等比缩放的历史问题关联 issue #807、#875。上述getRectResizeEdgePoint等比缩放计算仍是理解端点重算原理的核心因此文档保留了这份讲解。缩放结束时会执行一次网格吸附Control.tsx 的onDragEnd按gridSize将节点中心取整归位并再调用一次updateEdgePointByAnchors修正锚点滞后问题。四、三种图形的边端点重算算法矩形、椭圆、菱形的数据结构和绘制逻辑不同因此边端点重算的数学方法各不相同——这是实现节点缩放最复杂的部分。相关算法集中实现于 control/Util.ts下文逐一说明原文档中的示意图为外链图片此处以算法描述还原其数学含义。4.1 矩形按边比例 圆角角度以矩形中心为原点 (0,0)分别处理直边与圆角两种情形getRectResizeEdgePoint直边情形判断端点落在哪条边上左/右/上/下保持端点在边上相对中心的比例 pct 不变。例如端点在上边时const pct (x - beforeNode.x) / (beforeNode.width / 2 - radius) afterPoint.x afterNode.x (afterNode.width / 2 - radius) * pct其中radius为矩形圆角半径afterNode.width / 2 - radius是去除圆角后的有效直边半宽端点不会落入圆角区域之外。圆角情形先根据端点相对四个圆角圆心的位置确定所属圆角左上/右上/右下/左下再利用端点与圆心的夹角 θ 不变重算新坐标const angle Math.atan2(y - beforeCoc[index].y, x - beforeCoc[index].x) afterPoint.x afterCoc[index].x radius * Math.cos(angle) afterPoint.y afterCoc[index].y radius * Math.sin(angle)4.2 椭圆保持夹角 θ 不变以椭圆中心为原点 (0,0)先计算缩放前端点与 x 轴的夹角 θ缩放后保持夹角不变计算新坐标getEllipseResizeEdgePoint// 将椭圆中心当作中心点(0,0)放大缩小前点与X轴夹角 const tan (point.y - beforeNode.y) / (point.x - beforeNode.x) const a rx // 长半轴 const b ry // 短半轴 if (tan Infinity) { // θ PI / 2 x 0; y b } else if (tan -Infinity) { // θ 3 * PI / 2 x 0; y -b } else if (point.x - beforeNode.x 0) { // 一、四象限 x (a * b) / Math.sqrt(b * b a * a * tan * tan) y (a * b * tan) / Math.sqrt(b * b a * a * tan * tan) } else { // 二、三象限 x -(a * b) / Math.sqrt(b * b a * a * tan * tan) y -(a * b * tan) / Math.sqrt(b * b a * a * tan * tan) } afterPoint { x: x afterNode.x, y: y afterNode.y }公式推导基于椭圆参数方程已知斜率为 tan 的直线与椭圆x²/a² y²/b² 1的交点即缩放后同夹角下新端点所在位置。4.3 菱形保持比例 pct L/NE 不变以菱形中心为原点 (0,0)计算点 P 到参考点 E 的距离 L再求 L 与 NE 距离的比值 pct缩放后保持该比值不变重算坐标getDiamondResizeEdgePoint。当点 P 的横坐标大于 0 时计算比例的参考点是 E右顶点小于 0 时参考点取 W左顶点。纵坐标大于 0 取上顶点方向否则取反。const px point.x - beforeNode.x const py point.y - beforeNode.y const pct Math.sqrt((rxBefore - Math.abs(px)) ** 2 py * py) / Math.sqrt(rxBefore * rxBefore ryBefore * ryBefore) // …缩放后按 rxAfter/ryAfter 与 pct 反推出新的 x/y五、配置项详解5.1 缩放范围Resize Range可通过sizeRange限制拖拽时节点尺寸的最小/最大值触达边界后节点尺寸不再变化。支持配置与默认值如下sizeRange: { rect: { minWidth: 30, minHeight: 30, maxWidth: 300, maxHeight: 300, }, ellipse: { minRx: 15, minRy: 15, maxRx: 150, maxRy: 150, }, diamond: { minRx: 15, minRy: 15, maxRx: 150, maxRy: 150, }, }对照源码RectResize.tsx可知模型层在initNodeData中设置了与文档略有差异的默认值——矩形minWidth/minHeight为 30maxWidth/maxHeight为2000椭圆与菱形同样为 30~2000min 与 max 的取值决定了freezeWidth/freezeHeight是否生效。因此文档中的sizeRange更接近示例默认值实际以源码initNodeData为准业务上可通过覆写模型属性自定义。范围约束的实时校验位于更新方法中如 Control.tsx一旦计算出的尺寸越界调用this.dragHandler.cancelDrag()取消本次拖拽避免鼠标位置与节点缩放不一致的抖动。5.2 拖拽步长Dragging Step步长为 n 时节点坐标按 n/2 更新默认步长为 2保证缩放后节点坐标为整数设置了网格如对齐场景时默认步长变为 2 * grid。当 grid 大于 10 时缩放手感可能变肉此时可手动调小步长在对齐与顺滑之间取平衡。源码中的印证Control构造器注释明确写道为保证对齐线功能正常使用step 默认是网格 grid 的两倍没有网格设置默认为 2保证坐标是整数Control.tsx。当前实现通过StepDrag控制拖拽粒度并在onDragEnd用gridSize对节点中心做取整吸附。5.3 样式定制插件内置一组默认主题样式使选中缩放态整体观感更协调可覆写lf.setTheme({ rect: { strokeWidth: 2, outlineColor: transparent, }, ellipse: { strokeWidth: 2, outlineColor: transparent, }, diamond: { strokeWidth: 2, outlineColor: transparent, }, })选中态的外框与控制点样式支持进一步定制默认值如下style: { outline: { stroke: #000000, strokeWidth: 1, strokeDasharray: 3,3, }, controlPoint: { width: 7, height: 7, fill: #FFFFFF, stroke: #000000, }, }从源码看样式实际上收敛为两个可覆写的模型方法而非全局 style 对象getResizeOutlineStyle()缩放选中态的外框虚线框样式。矩形默认stroke: none椭圆与菱形默认fill: none; stroke: transparentRectResize.tsx、EllipseResize.tsxgetControlPointStyle()四角控制点样式默认width/height: 7, fill: #FFFFFF, stroke: #000000RectResize.tsx。同时缩放节点在静默模式isSilentMode下会隐藏外框与控制点如 EllipseResize.tsx 仅在isSelected !isSilentMode时渲染ControlGroup。六、事件node:resize节点缩放完成后触发node:resize事件提供节点缩放前后的基本信息、尺寸与位置便于宿主系统做联动处理。事件的完整载荷在 Control.tsx 的eventEmit中构造this.graphModel.eventCenter.emit(node:resize, { preData: oldNodeSize, // 缩放前id / modelType / type / 位置与尺寸 data: newNodeSize, // 缩放后同上 deltaX, deltaY, index: this.index, // 被拖拽的控制点索引 model: this.nodeModel, })监听方式使用教程lf.on(node:resize, ({ preData, data }) { console.log(preData, data) })preData/data中包含的字段及含义名称类型说明idstring节点 idtypestring节点类型modelTypestring节点图形类型内部定义xnumber节点中心 x 坐标ynumber节点中心 y 坐标rxnumberx 轴半径椭圆、菱形rynumbery 轴半径椭圆、菱形widthnumber节点宽度矩形heightnumber节点高度矩形七、自定义节点接入缩放能力扩展包导出了RectResize、EllipseResize、DiamondResize以及源码中的HtmlResize每种节点都包含view与model两个属性。开发者只需继承对应节点的.model与.view即可获得缩放能力。以矩形为例从不可缩放的自定义节点改为可缩放的自定义节点仅需替换继承基类使用教程// 之前不可缩放 import { RectNode, RectNodeModel } from logicflow/core class CustomNode extends RectNode {} class CustomNodeModel extends RectNodeModel {} export default { type: custom-node, model: CustomNodeModel, view: CustomNode } // 之后可缩放 import { RectResize } from logicflow/extension class CustomNode extends RectResize.view {} class CustomNodeModel extends RectResize.model {} export default { type: custom-node, model: CustomNodeModel, view: CustomNode }7.1 设置节点形状属性LogicFlow 将节点的宽高、半径等称为形状属性可通过覆写 initNodeData 或 setAttributes 设置。但可缩放节点的宽高只能在initNodeData中设置不能在setAttributes中设置缩放机制会覆盖该时机class ResizableRectModel extends RectResize.model { initNodeData(data) { super.initNodeData(data) this.width 100 this.height 40 } }7.2 自定义节点外观覆写 getResizeShape对样式复杂的节点自定义渲染通常覆写view的getShape而可缩放节点因为需要把控制点组叠加到图形上必须覆写getResizeShape而不是getShapeimport { RectResize } from logicflow/extension class ResizableRectModel extends RectResize.model { initNodeData(data) { super.initNodeData(data) this.width 100 this.height 40 this.text.draggable true } } class ResizableRectView extends RectResize.view { getResizeShape() { const { model } this.props const { x, y, width, height, radius } model const style model.getNodeStyle() return h(g, {}, [ h(rect, { ...style, x: x - width / 2, y: y - height / 2, rx: radius, ry: radius, width, height, }), ]) } } export default { type: resizable-rect, view: ResizableRectView, model: ResizableRectModel }继承HtmlResize的节点自定义view时继续使用自定义 HTML 节点 view 的setHtml方法即可。7.3 设置缩放最大/最小值v1.1.8在模型层直接赋值maxWidth/maxHeight/minWidth/minHeightclass ResizableRectModel extends RectResize.model { initNodeData(data) { super.initNodeData(data) this.width 100 this.height 40 this.maxWidth 400 this.maxHeight 400 } }7.4 调整缩放灵敏度gridSizev1.1.8可为单个节点设置gridSize属性控制鼠标移动多少距离后才开始缩放节点class ResizableRectModel extends RectResize.model { initNodeData(data) { super.initNodeData(data) this.gridSize 400 } }默认情况下LogicFlow 为保持节点整齐、便于对齐要求鼠标位移达到初始化画布gridSize的两倍才改变节点尺寸这会让缩放手感偏钝在不改动画布全局gridSize的前提下为单个节点单独调小gridSize可使缩放更顺滑源码中菱形模型即单独设置了this.gridSize 1见 DiamondResize.tsx。7.5 等比缩放源码扩展能力除文档内容外源码还提供了enableProportionResize(turnOn true)方法RectResize.tsx开启后节点按百分比widthPCT/heightPCT等比缩放并依据最大/最小尺寸自动计算缩放上下限。该方法要求在重设宽高与最大最小限制之后再调用不建议放在initNodeData中。八、总结NodeResize 是 LogicFlow 扩展机制的一个典型范例通过继承内置节点 覆写 getShape 叠加控制点 更新模型属性 重算边端点在不侵入核心引擎的前提下为节点赋予了缩放能力。其核心要点可以归纳为四点MVVM 数据流视图拖拽 → 模型属性更新x/y、textPosition、width/height 或 rx/ry、边端点→ 视图重绘三种图形的差异化算法矩形按边比例 圆角角度、椭圆按保持夹角、菱形按保持 L/NE 比值全部集中在 control/Util.ts丰富的定制入口缩放范围、步长、外框/控制点样式、node:resize事件、getResizeShape/getResizeOutlineStyle/getControlPointStyle覆写点演进方向插件已标记弃用2.0 将把缩放能力内置到核心NodeResize/index.ts但本文的算法原理与自定义模式仍具有长期参考价值。若你对插件的实现有想法或建议可在 LogicFlow 社区用户群中交流讨论。【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表