Files
envi-code/XEquipment-refactor-plan.md
T
company-desktop cee49e6e63 修正 XEquipment 重构计划的核对错误并补充遗漏项
- 3.2:改为 4 个类型有 getInterfaces 重写(DLD_FZ/DMF_56/YUANXINGGUAN/FANGXINGXIANG),补全 m_interfaces 四个写入点,删除 ent2db 未纳入支持列表的错误说法,新增 dwgIn 读入接口被工厂 getInterfaces 覆盖的顺序缺陷

- M1/M2/M3:修正修法——工厂改为先建后替(default 分支不能先删)、copyFrom2 判空、copyFrom 走基类后仅在 impl 为空时补建(基类 CopyObject 会经 dwgInFields 重建)

- M7:补 XL 的 %d 与 double 实参 ABI 错配;M8:补 ParamEquImpl.h:42 同名宏与 IMP_XDATA_KEY 说明

- 修正参考行号(AecDbParamEquipment.cpp:162)、子类计数(46=45+1)、验证 preset(R230→R190)与 cmake 命令语法;新增 L6 与 subTransformBy 接口未跟随移动
2026-10-26 13:55:24 +08:00

139 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# XEquipment(外壳 / Impl 分离机制)分析与重构计划
> 分析对象:`AecDbXEquipment`(外壳,`AcDbEntity` 派生) + `XEquipmentImplBase` 及其 46 个子类(45 个已接入工厂 + 1 个死类 XICHENZHAO_I,Impl 策略层)。
> 数据流:全部参数化数据存于基类 `m_mapSpecXData`(`AecDbObjXDataMap`),Impl 通过 `m_pXDataMap` 指针反向读写,不拥有数据。
> 核对基线:main @ 20dccfa。文中行号已按该版本逐条核对;带“核对”字样的条目为本次复核后修正的内容。
## 0. 涉及文件
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `inc/implement/AecDbXEquipment.h` | ~280 | 外壳类声明(设备类型 enum + 成员声明) |
| `CoreLibrary/AecDbXEquipment.cpp` | 6 | 仅 #include 分发:NDEBUG 时包含实现文件,否则链 CoreImplement.lib |
| `CoreLibrary/Implement/i_AecDbXEquipment.cpp` | 1001 | 外壳实现(构造/析构/dwgIn-Out/copyFrom/夹点/绘制) |
| `CoreLibrary/Implement/XEquipmentImpl.h` | 458 | Impl 基类 + 46 个子类声明 + DECLARATION_SUBCLASS / DEF_KEY_FUNC 宏 |
| `CoreLibrary/Implement/i_XEquipmentImpl.cpp` | 9832 | Impl 实现(匿名 namespace 几何工具 + 各设备 draw3d / getUniqueName / GetSpec) |
关联设施:`inc/AecDbEwPipeBase.h/.cpp`(m_mapSpecXData、m_pGripAppData 数组、管嘴基类设施)、`AecRxBase/AecDbObjXDataMap.cpp`(GetAt 语义)、`Pool3dCmd/ParamEquJson.cpp`(createParamEquImpl 的外部调用方)。
## 1. 机制概述(现状)
数据与行为彻底分离:外壳 `AecDbXEquipment : AecDbBDPipeBase : AecDbEwPipeBase : ... : AcDbEntity`。
- 工厂:createParamEquImpl(type, bRedraw) 用 _CREATE_ 宏 switch 49 个 case,new 出 X<组>::<类>(&m_mapSpecXData),随后 setType 重写 KEY_TYPE、getInterfaces、可选 draw3dByImp()。
- 绘制链:draw3dByImp() -> RemoveAllX3dPart() -> impl->draw3d(AcDbVoidPtrArray&) -> 各设备用匿名 namespace 几何工具(makeXSolid_by_point / makeXSolid_by_region / drawXCylinder / drawXRevolved / add2Parts 等)生成 AcDbX3dSolid*,包装成 AecDbX3dPart*,所有权经 ar 转移给外壳 m_arX3DPartPtrs 缓存。Draw3d / GetSectionDraw / subGetGeomExtents / HLR 全部从这个 3D buffer 取数。
- 克隆:cloneImpl(pXData) -> m_pImpl->makeSelf(pXData)(每个子类 new 同类型对象、指向新 xdata map)。
- 序列化:dwgOutFields 只写 version + m_interfaces,参数全靠基类 m_mapSpecXData 的 dwgIn/Out;dwgInFields 读完后 DELETE_PTR(m_pImpl) 再按 GetType() 重建。dxfIn/dxfOut 均为空实现。
- copyFrom2 才是“真正的深拷贝”(拷 xdata + 克隆 impl);虚函数 copyFrom 只拷 m_interfaces。
- getUniqueName:impl 侧把全部参数格式化后 MD5,外壳再拼上 X3dPart 数量。缓存键的基础设施已存在,但当前没有缓存层消费它。
## 2. 内存管理分析
### 2.1 m_pImpl 生命周期(new/delete 配对)
| 位置 | 操作 | 评价 |
| --- | --- | --- |
| ~AecDbXEquipment | DELETE_PTR(m_pImpl) | 正确 |
| dwgInFields | DELETE_PTR 后重建 | 正确 |
| copyFrom2 | m_pImpl = cloneImpl(...),无 DELETE | M2 |
| createParamEquImpl 内部 | m_pImpl = pImp,无 DELETE | M1 |
| ToJson | clone() + copyFrom2 | 新对象 m_pImpl==NULL,暂时安全 |
M1(中)createParamEquImpl 覆盖前不释放旧 impl。要区分两类调用:inplace 编辑改参数走的是“对图中已有实体二次调用”(ParamEquJson.cpp:345/2015/2186 等),旧 impl 每次泄漏;而 311/8604/9526 是 new 出新对象后的首次调用,m_pImpl 为 NULL,不构成泄漏,不能混为一谈。impl 对象小(一个 map 指针 + 虚表),每次几十字节,但每次编辑都漏,长时间作业会累积,且形成“赋值即丢失”的坏模式。修:不要在“函数入口”直接 DELETE_PTR——switch 的 default 分支会 return NULL,先删会把原本可用的 impl 弄没。正确做法是先 new 出 pImp、走完 switch,再在 :650 处 DELETE_PTR(m_pImpl); m_pImpl = pImp;。
M2(中)copyFrom2 同样不释放旧 impl。当前唯一调用点 ToJson 作用于 clone() 出的新对象所以没暴露,但它是 public API。修法同 M1(先克隆成功再替换);另注意 cloneImpl 在源对象无 impl 时返回 NULL(i_AecDbXEquipment.cpp:572-577),替换前要判空,避免把已有 impl 换成 NULL。
M3(高)copyFrom 切断基类拷贝链且不重建 impl。AecDbXEquipment::copyFrom 只做 m_interfaces = pSrcEqu->m_interfaces,没有调用 AecDbBDPipeBase::copyFrom(基类链 AecDbBDPipeBase::copyFrom -> AecDbEwPipeBase::copyFrom -> AecDbCurveBase::copyFrom + CopyObject 负责深拷 m_mapSpecXData 等),也没有重建 impl。后果:任何走 copyFrom 的路径(AcDbEntity::clone() 就是 create+copyFrom;部分 reactor / 夹点编辑路径)得到的设备是空壳:KEY_TYPE 丢失(GetType()=0)、m_pImpl=NULL、X3dPart 空。目前只有 ToJson 用 clone() 且紧跟 copyFrom2 兜底 + 手动补 X3dPart 才没爆。修:copyFrom 里先调 AecDbBDPipeBase::copyFrom(pSrc)。注意基类链里的 CopyObject(AecDbEwPipeBase.cpp:1529)是 dwgOut→dwgIn 往返,虚分发会重新进入 AecDbXEquipment::dwgInFields,那里已经 DELETE_PTR(m_pImpl) + createParamEquImpl(GetType(), false)(:170-178);所以基类返回后 impl 通常已重建,只需 if (!m_pImpl) createParamEquImpl(GetType(), false); 兜底,不要再无条件删一次(否则每次 clone 多建一次几何)。可参考兄弟类 AecDbParamEquipment::copyFrom(AecDbParamEquipment.cpp:162)的“拷 XDataMap + cloneImpl”写法。
M4(低,契约风险)impl 与 xdata 的所有权分离是隐式契约。impl 只存 AecDbObjXDataMap*(指向外壳 m_mapSpecXData 地址)。makeSelf 克隆时传入新外壳 map 指针,契约成立;但任何“把 impl 指针直接搬给另一个外壳”的做法都会读写旧外壳数据。目前无此路径,属潜在雷区。建议在 XEquipmentImplBase 定义处注释固化契约。
M5(中)draw3d 中途失败时缓存处于“部分更新”状态。draw3dByImp 先 RemoveAllX3dPart 再让 impl 往里 add;impl 的 draw3d 里有大量 if (!pSolid) return;(如 XGC 参数不足、TESTXDATA 失败直接 return),此时部分 part 已入缓存、其余丢失,设备显示残缺且无任何提示。修:draw3dByImp 改用临时数组,成功后一次性提交,失败保留旧缓存并记日志。
M6(低)ToJson 的 clone+copyFrom2+手动补 part 流程脆弱。i_AecDbXEquipment.cpp:770-810:clone()(此刻是空壳)-> copyFrom2(补 xdata+impl)-> Clean3DBuffer -> 手动把 this 的 X3dPart CopyFrom 到 p -> 移原点 -> 转方向 -> 又把 Pos/Dir 写回 xdata 抵消前面的移动。步骤间有隐含依赖。建议抽 makeTempClone() 辅助函数集中管理并注释“为什么不能直接用 clone()”。
M7(高,UB)getUniqueName 的变参 %s 分支是未定义行为。匿名 namespace 的 getUniqueName(i_XEquipmentImpl.cpp:455-540)手写 va_list 模拟 MD5 输入:%s 分支 ss = va_arg(vlist, TCHAR *),而 5 个调用点传的是 CString 对象:XGC:2287、BS:3154、XL:3692、GUANXIANG_I:8747、YUANXINGLABAKOU:9730。CString 是非平凡类型,MSVC x64 下经隐藏引用传递,va_arg(TCHAR*) 读到的是对象地址/内部指针字节而非字符串缓冲区,哈希输入是不确定的垃圾字节,可能读到非法内存。另 mem_size += ss.GetLength() * sizeof(TCHAR *)(每字符 8 字节)与实际写入 sizeof(TCHAR)(2 字节)不一致,过度分配不溢出,但说明这个手写变参没人审。另有同类 ABI 错配:XL 的 num 声明为 double 却用 %d 取(:3677/3692),va_arg(vlist, int) 在 x64 上读的是从未赋值的通用寄存器,同样是 UB。修:调用点统一传 (LPCTSTR)str;sizeof(TCHAR*) 改 sizeof(TCHAR);XL 的 %d 改 %f 或把 num 改成 int;或干脆重写为类型安全签名。
### 2.2 XDataMap 读取语义相关
M8(高)DEF_KEY_FUNC 宏返回未初始化值。宏体是 type v; m_pXDataMap->GetAt(key, v); return v;。实证 AecDbObjXDataMap::GetAt(AecDbObjXDataMap.cpp:1125/1163/1216/1245/1293)只在“键存在且 nVt 类型匹配”时才写出参,否则原样返回 FALSE。于是键缺失/类型不符时 GetH1() 之类返回栈上未初始化的 double / AcGePoint3d / AcGeVector3d,NaN 或垃圾坐标直接进入 sin/cos/normalize/setToAlignCoordSys,产出远离原点的实体或让 ASM 布尔失败。修:宏内值初始化(type v = {};)并对 m_pXDataMap 判空。影响所有 DEF_KEY_FUNC getter(XHG 14 个、SG 13 个、ZG 10 个参数等)。注意宏在 CoreLibrary/ParamEquImpl.h:42 还有一份完全相同的定义(AecDbParamEquipment 用),只改 XEquipment 这份等于没修干净;键本身由 IMP_XDATA_KEY(inc/define_class_xdata_key.h:5)定义,不需要补定义,只需初始化值并判空。
M9(低)m_pXDataMap 未判空。draw3d 大都有 if (!m_pXDataMap) return;,但 getter 没有;外部若用 NULL map 构造 impl,getter 直接崩。修:getter 判空返回默认值。
M10(低)GetType() 静默失败返回 0。int nType = 0; GetAt(KEY_TYPE, nType); 键缺失/类型不符则返回 0,createParamEquImpl(0) 走 default,m_pImpl=NULL,设备变空壳(后续都有判空不崩,但无声失败)。建议失败时 acutPrintf 日志。
### 2.3 夹点缓存(核查结论:安全,无需改动)
subGetGripPoints 把 m_pGripAppData 当数组写下标 i。基类 AecDbEwPipeBase 构造里 m_pGripAppData = new AecGripData[GRIP_LENGTH](256,AecDbEwPipeBase.cpp:406),析构 delete[](:425),且有 count >= GRIP_LENGTH 前置检查,不越界。仅建议把 GRIP_LENGTH 检查挪到更明显的位置(现在 count > eCommand19 - eCommand0 的二次裁剪逻辑有点绕)。
## 3. 代码逻辑分析
### 3.1 type 到 impl 工厂的覆盖缺口与同步成本
- header 21 个枚举组,switch 覆盖 18 组 49 case:
- FLUME(PARSHALL 水槽)、WEIR(三角堰/储留孔堰/包壁堰)无 impl 类,预期由独立实体类(AecDbRiverGroove 等)处理,确认无遗漏即可;
- CHANSHUDIBAN::YUANXINGDIBAN 有枚举、无 impl 类、无 case,死枚举,创建时 default 返回 NULL;
- XICHENZHAO_I impl 类存在(XEquipmentImpl.h:408)但无对应枚举组、无 case,死代码。
- 新增设备要同步维护三处(enum 链、switch case、impl 类),易漏。建议改注册表:static std::map<int, FnMaker> + 各命名空间静态注册对象,消灭手工同步;若维持 switch,至少 default: assert(false) 加日志。
- _CREATE_ 宏每个 case 都 setType(&m_mapSpecXData, type) 重写 KEY_TYPE:dwgIn 场景多余;inplace 场景会静默把 type 改成传入值,调用方传错 type 会污染 xdata 且无感知。建议宏里去掉 setType,由调用方显式 SetType。
### 3.2 管嘴(m_interfaces)生命周期缺口
- impl 侧 getInterfaces 基类是空实现(i_XEquipmentImpl.cpp:9736)。45 个已接入工厂的子类中只有 4 个重写:DLD_FZ(:8843)、DMF_56(:9038)、YUANXINGGUAN(:9538)、FANGXINGXIANG(:9630);其余 41 个类型调用它不产生任何接口(另有死类 XICHENZHAO_I 也未重写)。
- m_interfaces 的写入点共四处:dwgInFields(版本 2 以上从 dwg 读)、clearInterface(i_AecDbXEquipment.cpp:867,inplace 每次调用)、createParamEquImpl(:654)与 draw3dByImp(:745)里的 getInterfaces。
- 后果:(a) 41 个无重写的类型,命令新建设备时 m_interfaces 为空,管嘴夹点、管嘴捕捉拿不到接口(ent2db.cpp:379 已把 AecDbXEquipment 纳入导出列表,转换侧本身没问题);(b) inplace 编辑先 clearInterface 再 createParamEquImpl,41 个类型编辑一次就把从图纸读到的管嘴清空;(c) 版本 1 旧图(当时“读取 json 动态生成”)升级后接口丢失,无迁移代码从 _SRC_STRING_ JSON 重建。
- 另有顺序缺陷:dwgInFields 先 append 读到的接口(:163-175),再 createParamEquImpl(type,false)(:177-178),而工厂末尾会调 getInterfaces(:654)。DLD_FZ/DMF_56/FANGXINGXIANG 的实现是 removeAll 后重建,等于把刚序列化读入的管嘴丢掉(只有 YUANXINGGUAN 用 if (!infs.isEmpty()) return 挡住)。修工厂时要一并解决:getInterfaces 前判空,或给 createParamEquImpl 加 bFromDwg 开关。
- 建议:明确管嘴来源策略——要么给 41 个类型补 getInterfaces 按参数生成(虚函数位置本就是为这个留的),要么在 dwgIn version==1 分支从 JSON 迁移;同时修掉上面的覆盖顺序;并补“新建、保存、重开、inplace 编辑”的管嘴一致性用例。
### 3.3 变换语义
- subTransformBy 调 m_pImpl->transformBy(xform)(基类实现只更新 xdata 的 Pos):注释写“只移动X Y”但 pt.z = z; 被注释掉,实际 z 也变换,3D 移动/旋转会改标高,与注释矛盾。随后 AecDbBDPipeBase::subTransformBy 变换 X3dPart 缓存,两者各管一半(参数 vs 缓存),不重复,设计合理;需要决策的是 z 的语义:建议恢复 pt.z = z 或改注释,并与 SetElev 对齐。
- ToJson 里 vtDir.angleTo(kXAxis, kZAxis):vtDir 为零向量时行为未定义,SetPosAndDir / GetInsert 都不校验 Dir。
- subTransformBy 里接口变换被 if (m_pImpl) 包住(i_AecDbXEquipment.cpp:268-285):impl 缺失时 m_interfaces 不跟随移动,管嘴位置与设备参数脱节。建议把接口变换移出该判断。
### 3.4 绘制性能(与 bit1.diagsession 分析呼应)
- draw3dByImp 每次参数变化全量重画:RemoveAllX3dPart + impl->draw3d 重建全部 part(每个 part 是 region 到 extrude/loft/revolve/sweep 的 ASM 运算)。大图纸 N 设备乘每设备 10 到 40 个 part,打开/编辑时 ASM/tbb 占用显著(此前 diagsession 分析中 tbb 25.5% 采样的大头)。
- getUniqueName(参数 MD5 加 part 数)已提供缓存键基础设施但没有消费方。建议 part 级缓存(按 uniqueName 失效),或参数编辑时按“受影响 part”增量重画(二期)。
- add2Parts 用 acdbCurDwg()->ucsxdir()/ucsydir() 作为 part 的轴:设备加入非当前 dwg(块/外部参照)或之后改 UCS 再重画,轴的语义会漂移。建议 part 轴改用世界坐标或实体自身数据库的 UCS。
### 3.5 其它逻辑问题
- L1 SetProperties 恒返回 false:即使成功设置“标高”也 return false,调用方按返回值判断成败会被误导。应返回 true。
- L2 dxfIn/dxfOut 空实现:DXF 读写静默丢失全部数据。若无需求建议注释标明“不支持 DXF”。
- L3 GetDraw2dables 两个分支的所有权:与基类行为一致暂无 double-free,建议加注释标明所有权约定。
- L4 GetSectionDraw 每次重建投影:3D buffer 空时触发全量重画再投影,剖切命令反复调用开销大,可按 uniqueName 缓存。
- L5 GetSpec 的魔法下标:pickItemInsert(all, spec, 8/12/3/21/23...) 按固定下标从 JSON 参数数组取项(i_XEquipmentImpl.cpp:812-820 直接 all[idx]),服务端参数顺序一变就取错值且无校验。建议改按 name 查找。
- L6 copyFrom 只拷 m_interfaces(:243-256),不拷 m_nInfActive 等活动管嘴状态,clone 后“当前激活接口”丢失。
## 4. 重构计划(分阶段,增量推进)
遵循仓库既有约定:每次只改一小批、两个 base(AecDbEwPipeBase / AecDbBDPipeBase)零改动、只编 R190 Debug 验证。
### P0 安全修复(低风险高价值,先做)
1. M7:5 处 GET_UNIQUE_NAME 的 CString 实参改传 (LPCTSTR);getUniqueName 的 sizeof(TCHAR*) 改 sizeof(TCHAR)。
2. M8:DEF_KEY_FUNC 宏值初始化 + m_pXDataMap 判空(XEquipmentImpl.h:50 与 ParamEquImpl.h:42 两份都要改)。
3. M1/M2:createParamEquImpl 改为“先建 pImp、成功后替换”(不要入口删,default 分支会丢 impl);copyFrom2 判空 cloneImpl 结果后再替换。
4. L1:SetProperties 返回值修正。
5. M10:GetType 失败日志。
### P1 正确性修复(需回归测试)
6. M3:copyFrom 补基类调用 + impl 重建(重点回归:CAD 内 copy/paste、clone、ToJson 提资)。
7. M5:draw3dByImp 临时数组 + 失败回退。
8. 3.2 管嘴来源策略落地(impl getInterfaces 重写 或 v1 JSON 迁移,先与需求方确认)。
9. 3.3 z 语义决策(恢复 pt.z=z 或改注释),回归移动/标高用例。
10. 3.4 add2Parts 轴改世界坐标(回归 UCS 非默认时创建设备)。
### P2 结构优化(大工程,单独排期)
11. 工厂注册表化(消灭 enum/switch/impl 三处同步)。
12. part 级几何缓存(消费 getUniqueName),打开大 dwg 性能收益的主要来源。
13. L5 GetSpec 改按名查找;L4 投影缓存。
14. 死代码清理:XICHENZHAO_I、CHANSHUDIBAN/YUANXINGDIBAN(确认后删)。
15. M4:impl 的 m_pXDataMap 访问收敛 + 契约注释。
### 验证方式
编译:`cmake --preset R190 && cmake --build --preset R190 --config Debug --target CoreLibrary_BAL_REMOTE`;涉及工厂/接口改动时追加 `--target Pool3dCmd`(CoreImplement 按 `CODEBUDDY.md` 的 Debug 约定一起编)。功能回归覆盖:新建各类型设备、保存、重开、inplace 编辑、移动/复制、夹点、剖切、ToJson 提资、HLR;内存方面做长时间 inplace 编辑会话观察内存曲线(验证 M1/M2 泄漏消失),并单独核对“保存后重开,4 个有 getInterfaces 重写的类型管嘴数量是否与保存前一致”(验证 3.2 的覆盖顺序缺陷)。