· 连线端点(插槽)扩展评估:要不要为「母线 T 接」动引擎
本文是评估记录,不是实现计划。 起因:电力一次系统图(单线图)里,间隔是从母线任意位置 垂直引下的,而引擎现在只认 5 个固定插槽(T / R / B / L / C)——IED 的电力域包当前只能按 「母线中心 → 设备顶部」走线,出现水平绕行。本文回答三件事:改引擎的影响面有多大、 有没有不动引擎的做法、如果将来真要动引擎必须先满足什么。
结论速览
后续(2026-09-12,已完成):这条议题里的
getSerializableChildren()钩子已经落地(引擎 1.4.8): 「既是复合组件、又是容器」的组件(流程图 / BPMN 节点与池)可以实现它返回真实子节点, 序列化只写这些子节点、反序列化时派生部件仍由构造函数重建。修复了 BPMN 池/泳道快照往返丢元素 的真实 bug(实测 3 个元素只剩 1 个)。默认行为逐字节不变(不实现的组件照旧跳过全部子节点)。
落地状态(2026-09-12):方案 A 已在
ice-entity-designer0.0.33 落地 (电力域包:attachedBusId+ 几何贴合 + 应用层位移传播 + 隐式等电位),引擎一行未改。 落地过程中实测到一条新约束,反过来印证了「先别动引擎」的判断: 复合组件的序列化契约不允许它同时当容器 ——hasDerivedChildren() === true的组件, 序列化器会跳过它的全部子节点(Serializer.encodeRecursively),所以「把间隔收成母线的子节点」 这条路虽然运动学上成立(容器移动带动子节点,实测位移一致),但快照会丢设备(23 台掉成 15 台)。 结论:母线 T 接用「数据字段 + 几何判定」,既不碰引擎也能完整往返。若将来确实需要「复合组件当容器」 (例如 BPMN 泳道里再嵌一层带派生形状的容器),引擎应提供「只序列化真实子节点」的钩子 (如getSerializableChildren(),默认返回hasDerivedChildren() ? [] : childNodes)—— 这是一条独立的引擎议题,验收条件见 §5。
| 方案 | 改动面 | 风险 | 能力上限 | 建议 |
|---|---|---|---|---|
| A. 应用层:「母线 = 容器 + 隐式 T 接」 | 零引擎改动(全部在 IED 电力域包) | 极低 | 覆盖一次图的母线接间隔(本轮的需求) | ✅ 先做这个 |
B. 引擎:参数化插槽 B@0.25 | 3 个引擎 switch + 1 个应用 switch + FlowPort 类型 + 交互插槽 | 中(公共库、前向兼容缺口) | 沿边任意点连接(含跳线、跨线) | ⏳ 立独立议题,真要动就按 §5 的验收条件做 |
C. 引擎:端口提供者协议 component.getLinkAnchor(position) | 新增公开协议 + 全部调用点改走协议 | 中高 | 组件自定义任意锚点(3D、自定义形状…) | ❌ 现阶段不做 |
一句话:先把「母线接间隔」用容器语义解决(引擎一行不改),把「沿边任意点连接」留成独立的引擎议题—— 它不是不成立,而是它触碰的是所有下游工程共用的连线语义,必须单独排期、单独验收。
现状:端点是怎么定出来的(完整清单)
「插槽」不是引擎里一个集中的概念,而是一处字符串约定 + 三处 switch:
| # | 位置 | 作用 | 遇到未知 position 时的行为 |
|---|---|---|---|
| 1 | engine ICEPolyLine.followComponent() | 由宿主包围盒算出端点坐标 | default: 不动 → 端点是 [0,0](连线会跳到原点) |
| 2 | engine ICEPolyLine.dirVector() | 正交路由 / 贝塞尔控制点的出线方向 | default: → [0,0](无方向偏移,绕线变差但不炸) |
| 3 | engine ICELinkSlot.updatePosition() | 交互插槽(拖拽建线时的小圆点)的绘制位置 | default: → 不更新 |
| 4 | engine ICELinkSlotManager | 为可连接组件创建 5 个插槽、落点把 slot.state.position 写进 links | 交互上只提供这 5 个 |
| 5 | IED FlowDesigner.__slotPoint() | 应用层建线时的端点 | default: → 取中心点 |
| 6 | IED FlowPort 类型 | 'T' | 'R' | 'B' | 'L' | 'C' | 类型层面的白名单 |
| 7 | 序列化 / 快照 / 删除比对 / 跟随事件 | —— | 只当它是字符串:不校验、不枚举,原样存取 |
这决定了影响面:真正「懂」插槽语义的只有 3 个引擎 switch + 1 个应用 switch;
其余链路(序列化、快照、removeLink 的相等比较、AFTER_MOVE 跟随)对值是宽容的。
⚠️ 但要注意两件事:
- 安全网很薄:全仓只有 2 个测试文件触到插槽语义
(
tests/link/visio-bezier.test.ts、tests/graphic/ICEComponent.subtree-move.test.ts), 改动前必须补一批针对「端点解析 / 走线方向 / 交互插槽 / 序列化往返」的测试。 - 未知值的兜底是危险的:
followComponent的default会把端点算成[0,0]。 老引擎读到新格式(比如B@0.25)不会报错,而是静默把线画到原点——这类「不炸但画错」的兼容缺口, 比抛异常更难排查。
方案 A:母线 = 容器 + 隐式 T 接(零引擎改动)
做法:母线仍是一个宽矩形节点,但把接在这条母线上的间隔设备收 成它的子节点(ICEGroup.adoptChild()),
设备顶部引线直接落在母线底边上;电气语义由应用层定义——拓扑分析时把「父是母线」当成一条隐式连接,
不需要真画一段绕行线。
为什么这是「顺」的:
adoptChild()是引擎已有 API(BPMN 池/泳道就用它):只搬子树、不销毁,坐标由调用方换算;- 容器移动带动子节点是引擎已验证的能力(BPMN 那条 e2e:拖动池,泳道与里面的节点一起走);
- 视觉上,间隔的引线正好从母线垂直落下——和真实一次接线图的画法一致,比「在母线上打点再连线」更干净;
- 电气上「同一条母线上的所有间隔等电位」本来就是母线的定义,用父容器表达语义是准确的。
代价(都是应用层工作,不碰引擎):
- 电力域包要新增:
attachToBus(device, bus)(换算坐标 + adoptChild)、拖到母线附近自动吸附、 拓扑里把「父容器」当隐式边、校验里区分「用导体连接的设备」与「挂在母线上的设备」; - 母线节点变矮(10px 左右)时,点选/拖拽的命中区域偏小 → 应用层可以给母线加一点「命中 padding」 (或提供一个更粗的透明命中盒)。
可行性实测(本轮做的探针,跑完即删)
用 IED 的电力图元做了一次一次性验证(未提交代码):
| 检查项 | 结果 |
|---|---|
adoptChild 后子设备世界坐标不变(按父坐标换算局部坐标) | ✅ 位移 0px |
| 子设备上端点与母线底边中点水平对齐(即「垂直落下」) | ✅ 对齐误差 0px |
拖动母线 (120, 40) → 子设备位移 | ✅ 正好 (120, 40) |
也就是说:方案 A 不需要引擎提供任何新能力。
方案 B:引擎侧扩展插槽(评两个子选项)
B1 参数化插槽(推荐形态,如果要做)
position 支持 B@0.25(沿底边 25% 处)之类的形式,@ 缺省即 0.5 —— 与现有语义逐字兼容。
改动面(全部可枚举):
- 新增
parseLinkPosition(position):把'B'/'B@0.25'解析成{ side, ratio }, 未知输入回退到该边中点(顺手把 §1 里那个「跳到 [0,0]」的兜底修掉); followComponent()用它算端点;dirVector()只用 side;ICELinkSlot.updatePosition()同步;- IED
FlowDesigner.__slotPoint()与FlowPort类型同步放宽('B@0.25'用模板字面量类型收); - 交互(可选):拖线时在边上按鼠标位置生成临时插槽 → 工作量比 1–3 大得多,可先不做。
前向兼容缺口:新数据(B@0.25)在旧版本引擎里会落到 default → 端点算成 [0,0]。
可选缓解:写入时按「引擎版本」降级为 5 个固定插槽;或先只做 @0.5 之类的等价形式。
B2 命名细分插槽(用户提的「多加几组插槽」)
把 TRBL 扩成 T1..Tn / B1..Bn 之类。它和 B1 摸的是同一批代码,但:
- 一条母线可能有十几个间隔,档位要么开到很多(字符串丑、交互插槽满屏小圆点), 要么还是不够用;
- 真正的需求是「任意位置」,B1 只比 B2 多一个除法,却把上限打开了。
结论:要么不做,要做就做 B1,不做 B2。
方案 C:端口提供者协议(暂不做)
让组件自己实现 getLinkAnchor(position),默认实现就是现在的 5 个 case。
这是最通用的形态(自定义形状、3D、饼图扇区等都能用),但它把「插槽语义」从引擎的 3 处 switch
变成一个公开扩展协议,要考虑:协议版本化、第三方实现的健壮性、序列化与协议的对齐。
在没有真实第三方需求之前,投入产出比不高。
如果将来要动引擎(B 或 C),必须先满足的验收条件
- 现有 5 个插槽语义逐字节不变:老数据、老交互、老示例的输出与现在一致;
- 兜底从「跳到 [0,0]」改成「回退到该边中点」:任何未知 position 都不允许把线画到原点;
- 三处 switch 收敛到一个解析函数,避免以后再漏一处;
- 补测试:端点解析 / 路由方向 / 交互插槽 / 序列化往返 / 未知值兜底;
- 全量门禁 + 可视化回归(引擎
npm run verify+npm run test:visual:ci)全绿; - 三个下游(ice-entity-designer / ice-web-components / ice-chart)各跑一遍各自的门禁;
- 兼容矩阵:旧数据 → 新引擎、新数据 → 旧引擎 两种组合都有明确的、可解释的行为。
建议的顺序
- 先用方案 A 把电力一次图的「母线接间隔」画对(本轮已实测可行,零引擎改动);
- 等真出现「沿母线任意点」以外的需求(跳线、跨线、非矩形母线的复杂接线),再把 B1 立成独立的引擎议题, 按 §5 的七条验收;
- C 留到有明确的第三方扩展需求时再评估。