ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LogicFlow dynamic-group 多选拖拽感应区高亮修复:activeGroups 多组高亮的设计与实现

LogicFlow dynamic-group 多选拖拽感应区高亮修复:activeGroups 多组高亮的设计与实现 LogicFlow dynamic-group 多选拖拽感应区高亮修复activeGroups 多组高亮的设计与实现【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow本文围绕 LogicFlow 仓库中的设计文档 2026-07-03-dynamic-group-multi-select-sensor-design.md完整还原dynamic-group插件多选节点拖拽进组时感应区高亮随机失效问题的根因分析、设计方案与落地实现。读完本文你将掌握为何forEach顺序会导致高亮随机如何用Set集合 「纯计算 diff 更新」两阶段模式支持多分组同时高亮以及该修复在 插件源码 与 单元测试 中的真实落地形态。一、背景多选拖拽时感应区高亮随机消失LogicFlow 的dynamic-group插件提供了一种拖拽感应区sensor outline交互当用户拖拽节点进入某个分组节点的边界范围时该分组外围会亮起虚线描边默认橙色#feb663提示松手后此节点将入组。在单节点拖拽场景下该交互是稳定的但多节点框选拖拽selection整体拖动时感应区高亮出现不确定性 bug拖拽多个选中节点时究竟高亮哪个分组取决于forEach对选中节点的迭代顺序结果是随机的更糟的是某些组合下任何分组都不会高亮但实际 drop 后节点又确实入了组——视觉提示与最终行为不一致用户无法预判拖拽结果。二、问题根因onSelectionDrag的顺序覆盖设计文档指出根因在onSelectionDrag对每个选中节点依次调用setActiveGrouponSelectionDrag () { const { nodes: selectedNodes } this.lf.graphModel.getSelectElements() selectedNodes.forEach((node) { this.setActiveGroup(node) // 每次调用都会先关掉当前 activeGroup再尝试设新的 }) }而setActiveGroup内部的处理模式是先调用this.activeGroup.setAllowAppendChild(false)关掉当前高亮再决定是否点亮新的目标组。由于它保存的是单一引用activeGroup?多次调用时后一个节点的处理结果会覆盖前一个节点。典型失败场景选中节点 A位于 Group X 内和节点 B不在任何组内同时拖拽迭代顺序为[A, B]时迭代到 A → Group X 亮起迭代到 B → 调setAllowAppendChild(false)关掉 Group X随后发现targetGroup null提前 return最终 Group X 熄灭用户看不到任何高亮但 drop 后 A 会正确入组。反之若迭代顺序为[B, A]Group X 则会亮起。这正是结果随机的来源。三、行业方案调研多组同时高亮是更合理语义设计文档对主流图编辑工具的高亮策略做了对比工具高亮方式多组同时高亮React Flow每个节点独立调getIntersectingNodesCSS class 叠加✅draw.io鼠标光标位置单容器高亮❌LF 当前整个 selection 触发一次单activeGroup引用❌且有 bug结论React Flow 的做法最符合每个节点各自找目标组、结果独立的语义。即多选拖拽时如果节点 A 落在 Group X 内、节点 B 落在 Group Y 内那么 X 和 Y 应当同时亮起而不是互相覆盖。四、设计方案activeGroup单引用 →activeGroups集合核心思路将activeGroup单引用改为activeGroupsSetDynamicGroupNodeModel支持多组同时高亮。每帧拖拽时先纯计算出所有应该活跃的组无任何副作用再做一次diff 更新视觉状态只操作发生变化的组消除迭代顺序的影响。数据结构变更// Before activeGroup?: DynamicGroupNodeModel // After activeGroups: SetDynamicGroupNodeModel new Set()该结构变更已落地在 index.ts 的插件类成员 中注释明确写着激活态的 group 节点支持多组同时高亮。五、核心实现解析源码级设计文档中的四个改动点在 packages/extension/src/dynamic-group/index.ts 中均已实现。下面逐一对照。5.1 新增纯计算辅助方法getTargetGroupForNode将计算某节点目标组的逻辑从setActiveGroup中提取为纯计算、无副作用的辅助方法private getTargetGroupForNode( node: LogicFlow.NodeData, ): DynamicGroupNodeModel | undefined { const nodeModel this.lf.getNodeModelById(node.id) const bounds nodeModel?.getBounds() if (!nodeModel || !bounds) return undefined const targetGroup this.getGroupByBounds(bounds, node) if (!targetGroup) return undefined // 分组节点不能把自己设为目标组 if (nodeModel.isGroup targetGroup.id node.id) return undefined // 检查分组是否允许插入 if (!targetGroup.isAllowAppendIn(node)) return undefined return targetGroup }对应实现见 getTargetGroupForNode与设计稿逐行一致。三个关键判定值得展开getGroupByBounds通过边界检测找到节点所在的候选分组。当多个分组重叠时getGroupByBounds 会遍历候选组、取 zIndex 最高者作为目标组避免重叠区域归属歧义nodeModel.isGroup targetGroup.id node.id分组节点不能把自己设为目标组防止自包含targetGroup.isAllowAppendIn(node)调用分组模型上的准入规则。默认实现见 model.ts 的 isAllowAppendIn恒返回true业务可通过节点properties.isAllowAppendIn或重写模型方法自定义准入逻辑。5.2clearDragTargetHighlight重写遍历集合统一熄灭clearDragTargetHighlight() { for (const group of this.activeGroups) { group.setAllowAppendChild(false) } this.activeGroups.clear() }对应实现见 clearDragTargetHighlight。它把当前活跃集合里的每个组都关掉高亮再清空集合——不再像旧实现那样只关一个引用。该方法的调用点覆盖了所有拖拽结束路径onNodeDrop单节点 droponSelectionDrop多选 droponNodeMouseUp未 drop 即松开鼠标onNodeDndAdd拖放新增节点。5.3setActiveGroup重写单节点路径语义不变单节点拖拽onNodeDrag仍走此方法至多一个目标组语义与修复前一致但内部改用 Set diff 更新setActiveGroup (node: LogicFlow.NodeData) { const targetGroup this.getTargetGroupForNode(node) const next new SetDynamicGroupNodeModel() if (targetGroup) next.add(targetGroup) // diff 更新只变动有变化的组 for (const group of this.activeGroups) { if (!next.has(group)) group.setAllowAppendChild(false) } for (const group of next) { if (!this.activeGroups.has(group)) group.setAllowAppendChild(true) } this.activeGroups next }对应实现见 setActiveGroup由 onNodeDrag 在node:drag/node:dnd:drag事件中触发。5.4onSelectionDrag重写多节点路径修复核心 bug这是本次修复的核心。改成两阶段处理onSelectionDrag () { const { nodes: selectedNodes } this.lf.graphModel.getSelectElements() // 1. 纯计算每个节点独立找目标组结果合并为 Set const next new SetDynamicGroupNodeModel() selectedNodes.forEach((node) { const targetGroup this.getTargetGroupForNode(node) if (targetGroup) next.add(targetGroup) }) // 2. diff 更新只操作有变化的组避免无谓视觉抖动 for (const group of this.activeGroups) { if (!next.has(group)) group.setAllowAppendChild(false) } for (const group of next) { if (!this.activeGroups.has(group)) group.setAllowAppendChild(true) } this.activeGroups next }对应实现见 onSelectionDrag。对比旧版有两点本质区别纯计算阶段不做任何副作用先对每个选中节点独立调用getTargetGroupForNode把结果合并进同一个Set。由于Set天然去重且与顺序无关[A, B]和[B, A]会得到完全相同的结果diff 更新阶段只操作发生变化的组上一帧已亮且本帧仍亮的组不重复操作避免无谓视觉抖动本帧新增的组才点亮本帧消失的组才熄灭。5.5 事件注册与生命周期上述方法通过 init() 中的事件注册 接入 LogicFlow 事件体系lf.on(NODE_DRAG_EVENTS, this.onNodeDrag) // node:drag node:dnd:drag lf.on(EventType.SELECTION_DRAG, this.onSelectionDrag) lf.on(EventType.SELECTION_DROP, this.onSelectionDrop) lf.on(EventType.NODE_DROP, this.onNodeDrop) lf.on(EventType.NODE_MOUSEUP, this.onNodeMouseUp)其中NODE_DRAG_EVENTS定义在 constant/events.ts为node:drag与node:dnd:drag的组合事件名。destroy()中成对off注销保证插件销毁后不残留监听。此外 onGraphRendered 在整图重建lf.render/graphDataToModel时会重置插件侧状态其中包含this.activeGroups.clear()避免图数据切换后残留高亮引用。六、感应区高亮如何渲染底层联动要理解setAllowAppendChild(true)到底做了什么需要看模型与视图层的联动模型层setAllowAppendChild 设置observable groupAddable响应式标记。同时 getAddableOutlineStyle 提供感应区描边样式支持插件选项sensorOutline.stroke/strokeWidth定制未配置时回退到 DEFAULT_SENSOR_OUTLINE#feb663、线宽 2并固定使用strokeDasharray: 4 4虚线、fill: transparent视图层getAppendAreaShape 在groupAddable true时渲染一个比分组节点外扩 8px strokeWidth 的虚线矩形并拼入 getShape 的渲染结果中边界检测语义getGroupByBounds依赖 utils.ts 的 isBoundsInGroup判定条件是节点 bounds完全落在分组矩形范围内minX x - width/2等四项比较即完全在内才算命中感应区——这正是设计文档中bounds 检测是行业惯例的落地实现。七、行为对比修复前 vs 修复后场景修复前修复后A、B 都在 Group X 内同时拖拽随机亮 / 不亮Group X 稳定亮起A 进 GAB 进 GB随机只亮一个且可能被消除GA、GB 同时亮起A 进 GAB 不在任何组GA 被 B 的迭代消除不亮GA 亮起 ✓单节点拖拽正常不变行为一致drop 逻辑addNodeToGroup不涉及不变独立计算不依赖 activeGroups需要特别强调的是最后一行drop 逻辑完全不依赖activeGroups。addNodeToGroup 在 drop 时独立地再次调用getGroupByBounds完成入组判定与成员关系变更含同组内移动保持关系、跨组迁移、触发GROUP_NOT_ALLOWED事件等高亮只是预告入组才是事实二者解耦使得本次修复不会波及落盘行为。八、向后兼容与范围控制兼容性说明activeGroup属性被移除改为activeGroups。该属性无公开文档、不在类型导出中官方示例亦无直接访问plugin.activeGroup的用法影响极小clearDragTargetHighlight、setActiveGroup是内部方法签名不变、行为兼容drop 逻辑onNodeDrop、onSelectionDrop、addNodeToGroup不做任何修改。不在本次范围内明确排除设计文档列出了一系列经调研讨论后不纳入本次修复的改动避免范围蔓延项目原因改为鼠标坐标驱动的感应区检测draw.io / React Flow 均不采用现有 bounds 检测是行业惯例多节点 drop 原子性全部入组或全部不入组draw.io / React Flow 均采用各节点独立判断当前行为符合行业惯例autoResize从isRestrict门控解耦当前为有意设计有明确注释不在本次范围单节点感应区从完全在内改为中心在内低优先级独立评估九、验证方式单元测试 回归示例单元测试设计文档要求的手动验证点在 packages/extension/test/dynamic-group/sensor-outline.test.ts 中已有自动化覆盖测试通过lf.graphModel.eventCenter.emit(node:drag / node:drop / node:mouseup)模拟事件并断言group.groupAddable与activeGroups集合状态拖拽经过可入组分组 → 高亮亮起groupAddable true、activeGroups.has(group) true同组内拖拽后 drop → 高亮清除groupAddable false、activeGroups.size 0未 drop 即node:mouseup→ 高亮清除拖出分组边界 → 高亮清除sensorOutline插件选项自定义描边 →getAddableOutlineStyle()返回定制值未配置插件选项 → 回退DEFAULT_SENSOR_OUTLINE。这些测试即设计文档「验证方式」中单节点路径的自动化版本多选路径多组同时高亮可通过下方回归示例人工验证。回归示例仓库提供了专门的回归工作台 examples/dynamic-group-regression用于修复前后人工对比。启动方式# 仓库根目录 pnpm install # prepare 会自动 build:all # 或仅开发所需的最小构建 pnpm run build cd examples/dynamic-group-regression pnpm dev按设计文档的验证清单逐项确认多选节点拖入同一个分组→ 分组感应区稳定亮起多选节点分别拖向两个不同分组→ 两个分组同时亮起多选节点中部分在组内、部分在组外→ 有效的分组亮起组外节点不影响高亮单节点拖入分组→ 行为与之前一致。十、小结本次修复的本质是把高亮状态从单值引用升级为集合 两阶段更新语义上对齐 React Flow——每个节点独立判定目标组结果用Set合并天然去重、与顺序无关性能上通过 diff 更新每帧只对状态变化的组执行setAllowAppendChild避免重复操作引发的视觉抖动边界上严格解耦高亮预告与drop 落盘两条链路保证修复高亮不影响既有的入组语义与成员关系管理。该设计文档为 docs/superpowers/specs/2026-07-03-dynamic-group-multi-select-sensor-design.md核心实现集中在 packages/extension/src/dynamic-group/index.ts配合模型层 model.ts、视图层 node.ts、几何判定 utils.ts 与测试 sensor-outline.test.ts 即可完整理解该交互链路的全貌。【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表