Files
envi-code/AecDbEwEquipment-analysis.md
T

715 lines
43 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.
# AecDbEwEquipment 分析:内存、缺陷与性能
> 目标:在动手改之前,把 `AecDbEwEquipment`(阀门/设备通用实体)的内存问题、潜在缺陷和性能热点列清楚,给出可分批落地的方案。
>
> 范围:`SourceCode/Code2026/inc/AecDbEwEquipment.h`(578 行)、`SourceCode/Code2026/CoreLibrary/AecDbEwEquipment.cpp`(14290 行)。关联的 `AecEqui::XPrimData`(`inc/EarThwormDef.h:2526`)、`AecDbEntityCommon` 的 2D/3D 缓存(`AecRxBase/AecDbEntityCommon.cpp`)只在必要时提及。
>
> 状态(2026-10-22):仅分析,未改代码。行号以当前 `main` 为准。
---
## 1. 类职责与数据模型
### 1.1 职责
`AecDbEwEquipment` 继承 `AecDbEntityBase` + `AecMInterfaces`,是"阀门、设备"这类**参数化拼装体**的宿主:
- 自身只存插入点 `m_ptInsert`、方向 `m_vtDir`、专业数据 `m_mapSpecXData`、两个图层软指针 `m_idDash` / `m_idCenter`、竖放标志 `m_bVert`
- 几何是 `m_arSubPrimPtrs`(`CPtrArray`,元素是堆上的 `AecEqui::XPrimData*`),每个单元描述一个基本体(box / cylinder / cone / revolve / nozzle / extrude / Revit 组合 …)
- 3D 显示时逐个 `Draw3dSolid()` 造 `AcDb3dSolid`,按 `m_bBoolSubtract` 分成并集/差集两组,做布尔后放进 3D 缓存
- 2D 显示时按阀门类型画符号(25 个 `Draw*2d`),或把 3D 单元投影到平面(`PrimPorjectToPlan`)
### 1.2 几何单元 `XPrimData`
```cpp
struct XPrimData {
short m_eType; // 基本体类型
PVOID m_pData; // 参数块(new byte[n] 或 new 具体结构体)
int m_nSize; // 参数块字节数
...
std::vector<XPrimData*> m_Combins; // 组合设备的子单元
};
```
两个关键点,后文大量引用:
| 事实 | 位置 | 影响 |
| --- | --- | --- |
| `~XPrimData()` 只删 `m_Combins`,**不删 `m_pData`** | `EarThwormDef.h:2539` | 删除 `XPrimData` 前必须手动 `releaseData()`,否则参数块全泄漏 |
| `releaseData()` 按类型区分 `delete[]` / `delete` | `EarThwormDef.h:2544` | 释放方式与类型绑定,不能统一 `delete[]` |
### 1.3 数据流
```
开图 dwgInFields:1693 ──► dwgInFieldsForXPrim:172(递归读 m_Combins)
└─► subDMesh2NewSolid:3145(Revit 子网格 → 参数化单元)
存盘 dwgOutFields:1807 ──► dwgOutFieldsForXPrim:239(递归写 m_Combins)
绘制 Draw2d:2491 ──► MakeCurves:11063 ──► PrimPorjectToPlan:8314(逐单元投影)
Draw3d:3002 ──► Draw3dSolid:3905 ──► Draw3dSolid(XPrimData):3526(逐单元造体)
└─► 布尔并/减 ──► Add3DBuffer
导出 GetDraw2dables:2782 / GetDraw3dables:2928(克隆 + 投影/并体 + 写 BIM XData)
```
---
## 2. 内存问题
### 2.1 P0:`m_pGripAppData` 在 `dwgInFields` 里重复分配
| 位置 | 代码 |
| --- | --- |
| 构造 `:1682` | `m_pGripAppData = new AecGripData[GRIP_MAXNUM];` |
| `dwgInFields:1707` | `m_pGripAppData = new AecGripData[GRIP_MAXNUM];` ← 又分配一次,旧指针直接丢弃 |
| 析构 `:1690` | `delete [] m_pGripAppData;`(只删最后一块) |
`GRIP_MAXNUM` 为 256(`EarThwormDef.h:163`),`AecGripData` 含 `szTip[256]` + `szID[256]` + `szLockId[39]` + 3 个 `AcGePoint3d` + 1 个 `AcGeVector3d`,`sizeof` 约 1.2 KB。**每个设备实体开图即泄漏约 300 KB**,1000 个设备 ≈ 300 MB,且每次重开图纸累加。
同一处还有两个次要泄漏:
- `:1709` 读版本号失败、`:1712` 版本过高返回 `eMakeMeProxy` 时,新数组同样丢弃
- `:1720` `pFiler->readString(&pszTmp)` 分配的字符串没有 `acutDelString(pszTmp)`(对比 `AecDbEwFitting.cpp:639` 的正确写法),每次开图泄漏一个 GUID 长度的小块
**修复**:删掉 `:1707` 的重复分配(构造函数已经分配过);`readString` 用完补 `acutDelString`。改动 3 行,收益最大。
### 2.2 P0:析构函数不释放几何单元
```cpp
AecDbEwEquipment::~AecDbEwEquipment(void) // :1686
{
Clean3DBuffer();
Clean2DBuffer();
delete [] m_pGripAppData;
} // m_arSubPrimPtrs 完全没处理
```
`CPtrArray` 析构不会删除元素,`~XPrimData` 也不删 `m_pData`。所以**每个被销毁的设备实体,泄漏全部几何单元 + 参数块**。开图后关图、复制粘贴、`clone()` 的临时对象销毁,都走这条路。
同类 `AecDbEwFitting` 的析构(`:584`)同样没处理它的子单元数组,属于同一批遗留写法。
**修复**:析构里补 `FreeSubPrimData();`(成员函数,见 2.3 与 2.13)。
### 2.3 P0:`FreeBuffer(CPtrArray&)` 只释放数据块,不释放结构体
```cpp
void FreeBuffer(CPtrArray& arSubPrimPtrs) // :1520
{
for (...) {
AecEqui::XPrimData* pData = (AecEqui::XPrimData*)arSubPrimPtrs.GetAt(i);
if (pData != NULL) {
if (pData->m_nSize > 0)
delete[](byte*)pData->m_pData; // 只删 m_pData
}
}
arSubPrimPtrs.RemoveAll(); // 结构体本身没删
}
```
被 `copyFrom:2181` 调用,于是 `copyFrom` 每次都泄漏被覆盖的那批 `XPrimData` 结构体。修 2.2 时会连带放大这个问题。
另外这里无条件 `delete[](byte*)`,而 `releaseData()` 是按类型区分 `delete[]` / `delete` 的。
**核查 `m_pData` 的全部分配点后,结论是 `releaseData()` 的类型判断不可靠**——同一个类型在内存里可能是对象,也可能是字节数组:
| 来源 | 分配形式 |
| --- | --- |
| `dwgInFieldsForXPrim:206`(开图) | `new byte[n]`,所有类型 |
| `byJson:833`(JSON 导入) | `new PVOID[n]`,所有类型 |
| `copyXPrimData:150`(深拷贝) | `new byte[n]`,所有类型 |
| `CoreTools.cpp:2036` / `:4565` / `:4662` | `new byte[]` / `new char[]` |
| 各命令模块的 `setData<T>`(8 处) | `kExtrude`/`kRevolve` 用 `new byte[]`,其余 `new T` |
| `PipelineCmd/command.cpp:18670` 等 5 处 | `new AecEqui::XNozz` / `new AecEqui::XBOX` |
例如 `kBox` 从对话框创建时是 `new AecEqui::XBOX`,开图读回时却是 `new byte[]`。按 `m_eType` 判断,必然有一半场景不匹配(MSVC 下 POD 数组的 `delete` 与 `delete[]` 实际都走 `free`,所以一直没暴露)。
**改法:统一表示,再统一释放规则。**
1. 把 13 处对象形式的分配改成 `new unsigned char[sizeof(T)]`(`sizeof(T)` 与原来一致,后续 `(T*)m_pData` 的用法不变)
2. `releaseData()` 去掉 switch,只保留 `delete[] (unsigned char*)m_pData`
3. `~XPrimData()` 改为先调 `releaseData()`,顺带修掉 `m_Combins` 子单元的 `m_pData` 泄漏
这样 `AecDbEwEquipment.cpp` 里的 `ReleaseXPrimData` 就不需要了,直接 `pData->releaseData(); DELETE_PTR(pData);`。
### 2.4 P0:`subDMesh2NewSolid()` 删错元素,且泄漏
```cpp
void AecDbEwEquipment::subDMesh2NewSolid() // :3145
{
CPtrArray ar, subDs;
std::vector<int> idxs(0);
for (int i(0); i < m_arSubPrimPtrs.GetCount(); ++i) {
AecEqui::XPrimData *p = (AecEqui::XPrimData *)m_arSubPrimPtrs[i];
if (p->m_eType == AecEqui::kSubDMesh) {
idxs.push_back(i); // 记录的是"原始下标"
subDMesh2NewSolid(p, ar);
}
}
for (size_t i(0); i < idxs.size(); ++i)
m_arSubPrimPtrs.RemoveAt(i); // ← 用的是 i,不是 idxs[i]
...
}
```
两个错误叠加:
1. **删错元素**:`RemoveAt(i)` 删的是第 0、1、2… 个元素,而不是记录的 SubDMesh 下标。只要有一个 SubDMesh,就会把**第一个**正常单元删掉,SubDMesh 本身反而留着(后续 `Draw3dSolid` 对 `kSubDMesh` 是 `assert(0)`)。
2. **泄漏**:`RemoveAt` 只摘指针,被摘掉的 `XPrimData` 及其 `m_pData` 全部泄漏。
正确写法是按下标**从大到小**删,并先 `releaseData()` + `DELETE_PTR`。
这条只在"图纸里含 Revit 子网格单元"时触发,但一旦触发就是几何丢失,属数据正确性问题。
### 2.5 P1:`RemoveSubPrimObj` / `RemoveSubPrimObjByID` 不调 `releaseData()`
`:4276` 和 `:4291` 都是 `DELETE_PTR(pSolid)`,而 `~XPrimData` 不删 `m_pData`。删一个单元泄漏一个参数块(box/cylinder 几百字节,Revit 单元可能几十 KB)。
### 2.6 P1:`GetDraw2dables` 克隆后丢弃
`:2843` 对 2D 缓存里每个实体 `clone()`,然后:
- `AcDb3dSolid` → `OrthoProjected3DSolid` 后 `continue`(`:2850`)
- `AcDbSubDMesh` → `OrthoProjected3DSubMesh` 后 `continue`(`:2857`)
`OrthoProjected3DSolid` 只是把传入实体喂给 HLR 收集器(`CoreCommand/HlrCommands.cpp:214`),**不接管所有权**。这两条 `continue` 的克隆体全部泄漏,每次调用泄漏 N 个 solid。
### 2.7 P1:剖切相关的克隆泄漏
| 位置 | 问题 |
| --- | --- |
| `GetSectionDraw:12691` | `return OldGetSectionDraw(...)` 时没有 `delete pFit`(`:12677` 克隆的整份设备) |
| `OldGetSectionDraw:12620` | 逐个子实体 `clone()`,只对 `AcDb3dSolid` 做了投影,非 solid 的克隆体既不进结果也不释放 |
| `OldGetSectionDraw:12609` | `GetDraw3dables(arSubPtrs,false)` 产出的整组克隆从头到尾没释放 |
剖切是低频操作,但一次剖切可能泄漏几十个实体。
### 2.8 P1:`GetClosestPointToSUbPrim` 泄漏 region,并写库
```cpp
AcDbObjectId idSH = CoreTools::AddToModelSpace(pSolid); // :3986 把临时体写进模型空间
...
pFace->convertToRegion(regions); // :4032 新建 region
for (...) { ... } // regions 里的对象从不释放
delete pEnt; // :4049 只删了 explode 出来的本体
```
两个问题:
1. `convertToRegion` 产出的 region 全部泄漏
2. 为了算一个最近点,把临时 solid **加进当前图纸的模型空间**再 erase。这会触发数据库事务、undo 记录、反应器,是明显的性能与副作用问题
### 2.9 P2:`SortAecComplexCurves` 丢弃空曲线
`:11230` 先把非空曲线挑进 `std::vector`,然后 `arPrimCplPtrs.RemoveAll()` 再回填。**空曲线指针被直接丢掉,没有 delete**。`PrimPorjectToPlan:8507` 每次都调用,投影为空的单元越多,泄漏越多。
### 2.10 P2:`ByJson` 失败路径泄漏
`:12853` `new XPrimData`,`byJson` 返回 false 时不入数组也不删除,连带已解析出来的 `m_pData` 一起泄漏。另外 `ByJson` 没有先清空 `m_arSubPrimPtrs`,对已有几何的对象再导入会叠加重复单元。
### 2.11 P2:`MakeBlockRefSet` 失败路径泄漏
`:12540` `new AcDbBlockTableRecord`,若 `getBlockTable` 失败(`:12559`)或 `pTbl->add` 失败,`pRec` 既不关闭也不删除。`appendAcDbEntity` 失败时(`:12552`)实体也没关闭。
### 2.12 内存问题汇总
| 级别 | 问题 | 位置 | 触发频率 | 单次量级 | 状态 |
| --- | --- | --- | --- | --- | --- |
| P0 | 夹点数组重复分配 | `:1707` | 每个实体每次开图 | ~300 KB | 已修 |
| P0 | 析构不释放几何单元 | `:1686` | 每个实体销毁 | 全部参数块 | 已修 |
| P0 | `FreeBuffer` 不删结构体 | `:1520` | 每次 `copyFrom` | N 个结构体 | 已修 |
| P0 | `subDMesh2NewSolid` 删错+泄漏 | `:3159` | 含 SubDMesh 的图 | N 个单元 + 几何错乱 | 已修 |
| P1 | `RemoveSubPrimObj*` 漏 `releaseData` | `:4276` `:4291` | 编辑删除单元 | 1 个参数块 | 已修 |
| P1 | `GetDraw2dables` 克隆泄漏 | `:2850` `:2857` | 2D 导出/剖切 | N 个 solid | 已修 |
| P1 | 剖切克隆泄漏 | `:12691` `:12620` | 每次剖切 | N 个实体 | 已修 |
| P1 | `readString` 未释放 | `:1720` | 每次开图 | 小 | 已修 |
| P1 | `convertToRegion` 泄漏 | `:4032` | 最近点查询 | N 个 region | 已修 |
| P1 | `GetDraw3dables` 合并后不删入参 | `:2975` | 每次导出 3D | N 个 solid | 已修 |
| P2 | 空曲线丢弃 | `:11240` | 每次 2D 投影 | 小 | 已修 |
| P2 | `ByJson` / `MakeBlockRefSet` 失败路径 | `:12853` `:12540` | 异常路径 | 小 | 已修 |
“已修”的改动见 2.13。
### 2.13 第一批内存修复的实施记录
改动集中在 `CoreLibrary/AecDbEwEquipment.cpp`,不涉及头文件和存储格式。
| # | 实际改法 | 与原方案的差异 |
| --- | --- | --- |
| 1 | 删掉 `dwgInFields` 里的 `new AecGripData[GRIP_MAXNUM]`;`readString` 后补 `acutDelString(pszTmp)` | 同原方案 |
| 2 | 析构补 `FreeSubPrimData()` | 同原方案 |
| 3 | `FreeSubPrimData()` 成员函数:逐个 `releaseData()` + `DELETE_PTR`,最后 `RemoveAll` | 见 2.3,先统一了 `m_pData` 表示 |
| 4 | `subDMesh2NewSolid()` 按 `idxs` 倒序释放 + `RemoveAt` | 同原方案,另把 `size_t i` 改成 `int i` 倒序 |
| 5 | `RemoveSubPrimObj` / `RemoveSubPrimObjByID` 改用 `releaseData()` + `DELETE_PTR` | 同原方案 |
| 6 | `GetDraw2dables` 两个 `continue` 前补 `DELETE_PTR(pClnEnt)` | 同原方案 |
| 7 | `OldGetSectionDraw` 循环内补 `DELETE_PTR(pSubCln)`、循环后补 `FreeEntityArray(arSubPtrs)`;`GetSectionDraw` 两处 `return OldGetSectionDraw` 前补 `delete pFit` | 同原方案 |
| 8 | `convertToRegion` 的 `regions` 循环结束后逐个 `DELETE_PTR`;顺带把内层遮蔽的 `i` 改为 `j` | 同原方案 |
| 9 | `SortAecComplexCurves` 空曲线改为 `DELETE_PTR` | 同原方案,另补 `pCrv == NULL` 判断 |
| 10 | `ByJson` 循环前 `FreeSubPrimData()`,`byJson` 失败时 `releaseData()` + `DELETE_PTR` | 同原方案 |
| 11 | `MakeBlockRefSet` 的 `getBlockTable` / `pTbl->add` 失败时 `pRec->close()` 并返回 `false` | 同原方案 |
| 12 | `dwgInFields` 的 `m_arSubPrimPtrs.RemoveAll()` 改为 `FreeSubPrimData()` | 新增,原注释写着 FreeBufer 但没真释放 |
| 13 | `GetDraw3dables` 的 `booleanOper(kBoolUnite)` 之后补 `DELETE_PTR(pSolid)` | 新增,见 3.15 |
**函数命名整理**(`AecDbEwEquipment.cpp` 原来有 4 个文件作用域、外部链接的 `FreeBuffer*`,与其他 cpp 里的同名 `static` 函数容易混淆):
| 原名 | 现名 | 说明 |
| --- | --- | --- |
| `FreeBuffer(CPtrArray&)` | `AecDbEwEquipment::FreeSubPrimData()` | 只处理 `m_arSubPrimPtrs`,改成成员函数后参数不再需要 |
| `FreeBuffer(AcDbVoidPtrArray&)` | `static FreeEntityArray()` | 通用实体数组,加 `static` 收回内部链接 |
| `FreeBuffer2(CPtrArray&)` | `static FreeComplexCurveArray()` | `AecComplexCurve` 数组 |
| `FreeBuffer3(CPtrArray&)` | `static FreeFaceplanArray()` | `Aec3dFaceplan` 数组 |
**尚未处理**:
- 2.8 的第二个问题(为了算最近点把临时 solid 写进模型空间再 erase),属于副作用与性能,放在第三批
---
## 3. 潜在缺陷
### 3.1 P0:存盘时把组合设备的子单元清空(已修复,PR #48)
```cpp
void dwgOutFieldsForXPrim(AecEqui::XPrimData *pSolid, ...) // :239
{
...
if (CurrentVer >= 5)
{
// 做一下容错
if (pSolid->m_eType != AecEqui::kRevitCombination ||
pSolid->m_eType != AecEqui::kRevitCombination) // ← 恒真
pSolid->m_Combins.clear();
...
}
}
```
`x != A || x != A` 永远为真(第二个条件显然是想写 `kCombination`)。结果:
- **任何带 `m_Combins` 的单元,在存盘前子单元被清空**,写出去的就是 0 个
- 因为是就地 `clear()`,**当前内存里的对象也被破坏**,存完盘再画就没了
- 这段位于 `const` 的 `dwgOutFields` 调用链上,等于在"只读"函数里改数据
**影响面(2026-10-23 核实)**:`kCombination` 仍在用,`PipelineCmd/EquipmentMakeDlg.cpp:4357` 创建组合设备时会把被组合设备的子单元逐个 `push_back` 进 `m_Combins`,`toJson:789` 与 `Draw3dSolid:3844` 也都按这个列表递归。也就是说这条不是"可能有用",而是**新建的组合设备只要存盘一次,几何就永久丢失**(Revit 组合走 `kRevitCombination`,数据在 `m_pData` 里,不受影响)。
**已修复(PR #48)**:删掉整段容错,改为按实际数量写出,不再修改对象状态。没有按原方案"修成 `&&`",也没有把容错挪到读入,原因:
- `m_Combins` 只有组合单元才会非空,写端正确后读端不可能读到"非组合类型带子单元"的数据,容错没有作用对象
- 写端一旦按 `m_eType` 过滤,就会重现"类型判断错→丢数据"的同类风险
- 存盘函数改对象状态本身就是错的,无论条件对不对
顺带处理:跳过 `NULL` 指针并单独计数,避免"写了数量但循环少写一条"导致读写错位;删掉原来定义后未使用的 `int num`。
### 3.2 P1:`subErase` 里 `Find` 当布尔用(已修复)
```cpp
if (eSubType == eValve || sName.Find(_T("阀")) != -1 || sName.Find(_T("接头"))) // :2119
```
`CString::Find` 找不到返回 `-1`,非 0 即真,所以**第三个条件恒真**。整句等价于"永远是阀门",删除任何设备都会走合并管线那段逻辑(`GetInterface` + `PushGripMessage`)。应写成 `!= -1`。
**已修复**:补 `!= -1`。删除普通设备不再误走阀门/接头的合并管线分支。
### 3.3 P1:`DrawHandFlg` 用 `==` 赋值(已修复)
```cpp
if (nVt == 1) sFlag = _T("M");
else if (nVt == 2) sFlag = _T("G");
else sFlag == _T("Y"); // :2656 应为 =
```
液动(3)时文字为空,画出来是空标注。另外这里直接取 `GetInterface(0)` / `GetInterface(1)`,接口不足 2 个时 `inf2` 是默认值,`distanceTo` 为 0、`normalize()` 得到零向量,后续 `angleTo` 结果不可信。
**已修复**:`==` 改为 `=`,液动阀(3)现在会画出 `Y`。接口不足 2 个时 `inf2` 取默认值、方向不可信的问题未动,属于显示容错,另行评估。
### 3.4 P1:`ProcessHlrResult` 在遍历中删 map 元素(已修复)
```cpp
for (; it != _preAr.end(); ++it)
{
if (AcDbRegion::cast(it->first))
_preAr.erase(it); // :13133 迭代器失效,下一轮 ++it 是 UB
}
```
应改为 `it = _preAr.erase(it);`。这是典型的偶发崩溃点(容器元素少时不容易复现)。
**已修复**:改成 `while` + `erase(it++)`。没有用 `it = erase(it)`,因为 R190 走的是 C++98 标准(`cmakelists.txt` 按 SDK 版本切换),C++03 的 `map::erase(iterator)` 返回 `void`。
### 3.5 P1:`GetSectionDraw` 返回值取反(已修复)
```cpp
int nCount = arRegionPtrs.length();
GetSectionDraw(plan, vtTo, arRegionPtrs);
int nCoundPre = arRegionPtrs.length();
return (nCount - nCoundPre); // :12762 顺序反了
```
返回的是"调用前 − 调用后",恒为 0 或负数。若调用方用它判断"是否剖到东西",判断必然失败。
**已修复**:改为 `nCoundPre - nCount`。全库调用点都忽略这个返回值,所以此前没有实际故障。
注意:`AecDbEwFitting.cpp:16523`、`AecDbECFitting.cpp:8751` 的同名函数是一模一样的写法(同样的错误),本次未改,见 5.4。
### 3.6 P1:`HLR_CALC` 标志会被存进图纸
`Draw3d:3009` 一旦读到 `HLR_CALC` 为真就直接返回,什么都不画。这个标志由 `PreprocessForHlr:13204` / `:13360` 置真、`ProcessHlrResult:13107` 复位。
但 `m_mapSpecXData` 的序列化(`AecRxBase/AecDbObjXDataMap.cpp:2034`)是**全量写**,没有任何过滤。所以只要在"置真之后、复位之前"存盘(剖切过程中保存、崩溃、取消),标志就会留在 DWG 里,**下次开图这个设备在 3D 里永久不可见**。
建议:`HLR_CALC` 这类纯运行时标志不要进 `m_mapSpecXData`,或序列化时按 key 白名单/黑名单过滤。
### 3.7 P1:`GetInterface` 两个重载单位不一致(已修复)
| 重载 | 位置 | 是否乘 `dUnit`(米制 0.001) |
| --- | --- | --- |
| `GetInterface(int, AecInterface&)` | `:3323` | 是(`inf.dSize1 = pNoze->dVal1 * dUnit`) |
| `GetInterface(AcArray<AecInterface>&)` | `:3262` | **否**(直接 `pNoze->dVal1`) |
米制图纸里,同一份数据经两个入口读出来的接口尺寸差 1000 倍。`Draw*Vlv2d` 用下标版,`subErase` / `PreprocessForHlr` 用数组版,两边算出来的东西对不上。
**已修复**:数组版补 `* dUnit`,与下标版一致。
- 数组版里 `ptPos` 本来就 `transformBy(xform)` 缩放过,只有尺寸没缩,属于「位置按米、尺寸按毫米」的自相矛盾
- `dV` / `dA` 是电压电流(`EarThwormDef.h:518`),不是长度,不参与换算
- 影响面:数组版的 4 处内部调用(`getOsnapPoints`、`subErase`、`DrawInfArrow`、`vtAdjust`)只用 `ptPos` / `vtDir`;外部消费 `dSize1` 的是 `CoreCommand/pcf.cpp:1044` 的 `pcf_ewEquipment`,米制图纸导出的 PCF 管径此前大了 1000 倍
- 只在米制图纸(`INSUNITS == kUnitsMeters`)里有差异,毫米图纸 `dUnit` 为 1,行为不变
**顺带修复**:同一函数里 `inf.bSizeUnLock = true;` 原来写在循环外,循环内的 `memset(&inf, 0, sizeof(AecInterface))` 每次都把它清成 false,数组版返回的接口实际一直是「锁定」。已把赋值挪到 `memset` 之后,循环外那两行初始化(`bSizeUnLock` / `dGasket`)随之删掉——memset 之后本来就是 0。`bSizeUnLock` 目前只被 `i_AecDbXEquipment.cpp` 序列化,没有读取判断的地方,所以此前没有暴露成故障。
### 3.8 P1:`copyFrom` 漏拷字段(已修复)
`:2174` 只拷了 `m_mapSpecXData` / `m_nInfActive` / `m_ptInsert` / `m_vtDir` / 几何单元,**没有拷 `m_bVert`、`m_idDash`、`m_idCenter`**。复制粘贴一个竖放阀门,粘贴出来是横放的;两个图层软指针也丢了(虚线和中心线会跑到默认图层)。
**已修复**:补拷 `m_bVert` / `m_idDash` / `m_idCenter`。
### 3.9 P2:`MakeBlockRefSet` 的范围累加逻辑错(已修复)
```cpp
for (...) {
pEnt = (AcDbEntity*)arEntPtrs.at(i);
if (!AcDbBlockReference::cast(pEnt))
pEnt->getGeomExtents(extAdd);
if (isValidExtents(ext)) // :12522 判断的是累加器,不是本次结果
ext.addExt(extAdd);
}
```
- 第一轮 `ext` 还是默认(空)状态,`isValidExtents(ext)` 为假 → **第一个实体的范围永远加不进去**
- 若 `pEnt` 是块参照,`extAdd` 保持上一轮的旧值,会被重复累加
- 若整个循环没加进任何范围,`ptIns` 由默认范围算出,块原点落在原点而不是设备位置
应改成 `if (isValidExtents(extAdd)) ext.addExt(extAdd);`,块参照分支直接 `continue`。
**已修复**:块参照直接 `continue`;`extAdd` 每轮重新构造并检查 `getGeomExtents` 返回值;判断对象改为 `extAdd`。
### 3.10 P2:`byJson` 的 `m_pData` 尺寸对不上(已修复)
```cpp
p->m_nSize = it->value.GetInt(); // 字节数(来自 __nSize__)
GEO2JSON::binary br(it->value);
p->m_pData = new PVOID[br._len]; // 按 PVOID 个数分配
::memcpy(p->m_pData, br._data, sizeof(PVOID) * br._len); // :820
```
分配长度取决于 `br._len`,使用长度取决于 `m_nSize`,两者来自不同字段。JSON 被改过或版本不一致时就会越界读写。应按 `m_nSize` 分配并校验 `sizeof(PVOID)*br._len >= m_nSize`。
**已修复**:`br._len` 本来就是字节数,改为 `new byte[br._len]` + `memcpy(..., br._len)`,并用 `br._len` 覆盖 `m_nSize`(两者不一致时以实际数据长度为准)。
### 3.11 P2:开图读入没有边界校验(已修复)
`dwgInFieldsForXPrim:172`:
- `readInt32(&nSize)` 后直接 `new byte[pSolid->m_nSize]`(`:193`),超大值没有检查(负值被 `> 0` 挡住,不会分配)
- `readBytes` 的返回值没有检查
- `m_Combins` 是**递归**读(`:227`),没有深度上限,损坏的图纸可以打爆栈
同类的 `AcDbX3dSolid` 阶段一、二已经补过类似的防护,这里可以照搬。
**已修复**:加 16MB 上限(`kMaxXPrimDataSize`),超限 `setFilerStatus(Acad::eInvalidInput)` 让整个 `dwgInFields` 退出;`readBytes` 检查返回值,失败时释放并清零。`m_Combins` 的递归深度上限未加,损坏图纸仍可能打爆栈,优先级低。
### 3.12 P2:`PreprocessForHlr` 的"对面"被变换了两次(确认不改)
```cpp
AcGeMatrix3d mat4(mat3.inverse()); // mat3 = 沿截面法向平移 -H/2
for (...) {
AcDbEntity *pNew = (AcDbEntity*)pEnt->clone();
pNew->transformBy(mat4);
pNew->transformBy(mat4); // :13353 又乘一次
_other.append(pNew);
}
```
原实体在 `-H/2` 位置,克隆体被移到 `+H` 位置(两次 `+H/2`),两侧不对称。
**2026-10-25 结论:是故意的,不改。** `mat3` 只移动半个设备高度,乘两次正好凑成一个设备高度,剖切体的前后两面本来就该相距一个完整高度。
### 3.13 P2:`GetBodyBox` 恒返回 `TRUE`(已修复)
`:4205` 无论有没有取到实体都 `return TRUE`;且 `ar3D` 引用在 `Draw3d` 之前就绑定了(`:4207`)。当前 `Get3DBuffer()` 返回成员引用,重新填充不会换对象,所以能工作;但如果哪天改成返回副本就会静默失效。返回值也应反映实际结果。
**已修复**:返回实际是否取到范围;`_ext` 改为每轮新建,避免把上一轮的累积结果重复并入。11 处调用点都忽略返回值,行为不变。
### 3.14 P2:`clearGeoBuffer()` 是空实现(已修复)
`:12861` 函数体只有 `AssertWriteEnabled();`。`PipelineCmd/EquipmentMakeDlg.cpp` 有 8 处调用,本意应该是清掉 2D/3D 缓存(`Clean2DBuffer` / `Clean3DBuffer`)。现在是"调了没反应",改了参数后旧几何还在。
**已修复**:改为 `Clean2DBuffer(); Clean3DBuffer();`。调用方(`EquipmentMakeDlg`、`EQU_MODIFY`、`ParamEquJson`)都是改完参数后调它,语义一致。
### 3.15 P2:`booleanOper` 的所有权写法在本文件里自相矛盾
同一文件里两种写法并存:
| 位置 | 写法 | 若 SDK 会删除入参 | 若 SDK 不删除入参 |
| --- | --- | --- | --- |
| `:3790` | `booleanOper(...); DELETE_PTR(p);` | 二次释放 | 正确 |
| `:3883` `:3980` | `booleanOper(...)` 后统一 `deleteArrayVoidPtrs` | 二次释放 | 正确 |
| `:14198` | `booleanOper(...); DELETE_PTR(pSolid);` | 二次释放 | 正确 |
| `:2975` | `booleanOper(kBoolUnite, pSolid)`,克隆体不再处理 | 正确 | 每次泄漏一个克隆 |
本文件 5 处调用里 4 处都主动释放入参,`:2975` 是唯一的例外。
**2026-10-23 结论**:按「`booleanOper` 不删除入参」处理,`:2975` 补 `DELETE_PTR(pSolid)`(见 2.13 #13)。依据是同一文件另外 4 处(含失败路径 `:2002` 的 `DELETE_PTR(pSolidInside)`)都按调用方负责释放来写;若 SDK 实际会删除入参,这些既有代码早就在正常流程里二次释放崩掉了。
另外 `CoreLibrary/AcDbX3dSolid.cpp:1998` 的 `booleanOper` 成功路径没有释放 `pSolidInside`,同一约定下也是一个泄漏点,不在本次范围。
### 3.16 其他
- `GRIP_MAXNUM` 在两处定义:`EarThwormDef.h:163` 是 256,`AecWPipe.h:231` 是 24。当前 `AecWPipe.h` 没有被包含,暂时没冲突;一旦谁把它引进包含链,`m_pGripAppData` 的分配与使用边界就会不一致
- `ReadFromFile:4843` 的 `Adesk::Int32 id = ::_time32(NULL);` 是死变量,实际用的是 `++pX->m_nID`,同一秒导入多个单元时 ID 可能重复
- `Draw2d:2498` 的 `if (dc.IsDragging()) { /*return*/ }` 是空壳,拖拽时仍然读旧 2D 缓存,显示滞后一帧
---
## 4. 性能
### 4.1 3D 绘制链路
`Draw3d:3002` 每次调用:
1. `DrawInfArrow` → `GetInterface(arInfs)` → 每个管嘴一次 `GetInfFaceInf`
2. 有 3D 缓存就直接画缓存(快)
3. 没有缓存 → `Draw3dSolid:3905`:逐单元 `Draw3dSolid(XPrimData)` 造体(每个单元一次 `acisIn`/`createXxx`),有差集时再做一轮并集 + 一轮差集
4. `BoolUnit3DBuffer(0.001)`:米制时把缓存里每个 solid 再 `transformBy` 一次
5. 逐个 `setTransparency`
6. `Draw3DBuffers`
成本主要在 3:N 个单元 = N 次造体 + (N−1) 次布尔,与 `AecDbX3dPart` 的结论一致。
### 4.2 P0:拖拽时缓存不清理,且每帧全量重建(已修复,PR #53)
```cpp
if (!dc.IsDragging()) // 改前
{
if (Has3DBuffer()) { 画缓存; return; }
}
Draw3dSolid(&dc, nColor); // 拖拽时每帧都走到这里
```
而 `Add3DBuffer`(`AecDbEntityCommon.cpp:146`)只是 `append`,**不清理已有内容**。于是拖拽过程中:
- 每帧重新造 N 个 solid
- 每帧往 3D 缓存里**再追加** N 个
- `Draw3DBuffers` 把缓存里所有实体都画一遍
第 k 帧要造 N 个体、画 k×N 个体。内存随帧数线性涨、帧率随帧数线性掉。
**2026-10-25 已修复(PR #53)**:去掉 `!dc.IsDragging()` 判断,拖拽时也直接走 3D 缓存。原先写这个判断的原因是「解开后崩溃」,实际测试并不崩溃——崩溃来自 4.2 的缓存无限追加,而不是拖拽本身。缓存长度不再随帧数增长,`Draw3dSolid` 拖拽期间一次都不调用。
原计划的另外两个做法(拖拽时画线框、增量 `transformBy`)不需要了。
### 4.3 P1:2D 符号绘制触发 3D 布尔(暂缓)
11 处 `Draw*Vlv2d` 里调 `GetBodyBox(ext)`(`:5020` `:5189` `:5357` `:5512` `:5675` `:6307` `:6492` `:6654` `:6816` `:6996` `:7147`),而 `GetBodyBox` 在没有 3D 缓存时会 `Draw3d` —— 为了画一个 2D 阀门手柄的方向,把所有单元造一遍体再做布尔。
**2026-10-25 核实:原文的「缩放频繁时开销明显」写错了。** `Draw2d` 开头 `if (Has2DBuffer())`(`:2545`)就直接画缓存并返回,根本走不到 `Draw*Vlv2d`。缩放、平移、重生成都不会清缓存,只有改参数(`clearGeoBuffer`)、图层变化等才会。
所以准确的触发条件是:**2D 缓存失效后的第一次绘制**,且此时 3D 缓存也为空(平面图场景下 3D 缓存一直是空的)。是一次性开销,不是每次绘制的持续开销。
结论:暂缓。真要优化,收益只在「改完参数后立刻看平面图」这一下,不值得为此把 25 个 `Draw*2d` 的取尺寸逻辑改成参数 bbox。
### 4.4 P1:`GetInterface` 每次 `acedGetVar` + 临时实体(不做)
- `acedGetVar("INSUNITS")` 每个重载各调一次。**2026-10-25 决定不缓存**:`acedGetVar` 读的是数据库级变量,开销很小,而且当前图纸单位可以在会话中途变化(切换图纸、`INSUNITS` 被改),缓存反而要处理失效
- `GetInfFaceInf` 每个面调两次 `GetTransformbyXform`,每次构造一个临时 `AcDbLine` / `AcDbPoint` 再走 `TransformEntity`。一个 6 面 box = 12 个临时实体;夹点显示时每个管嘴都要算
**「单元 → 世界矩阵」是什么**:`TransformEntity`(`:1552`)对实体做的是一串矩阵变换,顺序固定:
```cpp
rotation(m_ptRotation.x, kXAxis) // 绕 X 轴转
rotation(m_ptRotation.y, kYAxis) // 绕 Y 轴转
rotation(m_ptRotation.z, kZAxis) // 绕 Z 轴转
translation(m_vtMove) // 平移到单元自身的偏移
m_arMatrix[i] // 逐项累乘附加矩阵
rotation(按 vtDir 先绕 Z 再绕法向) // 转到插入方向
translation(ptIns) // 平移到插入点
rotation(90°, kXAxis, ptIns) // 竖放时再翻一次
```
这串变换只依赖 `(pData, ptIns, vtDir, bVert)`,与具体是什么实体无关。现在为了变换「一个点」或「一个方向」,要先把它们塞进一个临时实体、跑完这一串 `transformBy`、再把结果读出来——矩阵算了一遍又一遍。
所谓「提成世界矩阵」,就是把这串矩阵按同样顺序 `postMult` 合成一个 `AcGeMatrix3d M`,之后直接 `ptGe.transformBy(M)` / `vtDir.transformBy(M)`。`GetInfFaceInf` 里 12 次临时实体可以全部去掉,只剩 12 次向量乘法。
这条**2026-10-25 决定不做**:`GetInfFaceInf` 只在夹点显示 / osnap 这类交互路径上跑,12 次临时实体的构造与 `transformBy` 相对于整条路径的开销占比很小,合成矩阵后省下的时间测不出来,反而多一份需要与 `TransformEntity` 保持同步的矩阵逻辑(后者一改,这里就错)。收益不明显,维持现状。
### 4.5 P1:`GetDraw2dables` 每次建 `*U` 块 + 跑 HLR(暂缓)
调 `CKTCadUtility::CreateBlockTableRecord(ar, ptIns, NULL, ...)`,`pszBlk == NULL` 时会在当前图纸里**新建一个匿名块表记录**(`KTCadUtility.cpp:537`)。也就是说一次"取 2D 图形"的查询操作会往 DWG 里写数据:块表增长、undo 记录、文件变大。同时 `OrthoProjected3DSolid` 走完整 HLR(`HlrCommands.cpp:214`),每个 solid 一次。
**2026-10-25 核实**:
- 剖切功能已经不再调用它(可以确认)
- 但它是 `AecDbEntityCommon` 的虚函数,`CoreCommand/KTEarthWormCmd.cpp` 里有十几处直接调用(`:6776` `:7884` `:12377` 等),`AecDbEntityCommon.cpp:1137` 的 `entitySet` 路径也在用,用途不止剖切
所以「剖切不用了」不等于「可以改语义」。暂不动,等确认其余调用方各自的期望。
### 4.6 P1:开图路径(不做)
`dwgInFields` 里无条件 `subDMesh2NewSolid()`,对每个 `kSubDMesh` 单元做一次 `ParseRevitFamily::parseJson`(`parseFamilyForRevit.cpp:1234`:RapidJSON 解析 + 遍历生成 `NewSolidBase`)。Revit 族转来的设备,这一步是开图耗时的大头之一。
**2026-10-25 评估结论:不能照 `AecDbX3dPart` 的方式惰性化。** 两者的性质不同:
| | `AecDbX3dPart` | `AecDbEwEquipment` |
| --- | --- | --- |
| 懒加载的对象 | `m_pSolid`,布尔运算的**派生结果** | `m_arSubPrimPtrs`,**持久化数据本体** |
| 延迟的代价 | 首次绘制时重算一次 | 对象在解析前处于「半初始化」状态 |
| 数组内容 | 不变 | `kSubDMesh` 被替换成若干新单元,**长度和顺序都变** |
具体风险:
1. `m_arSubPrimPtrs` 在本文件有 **99 处引用、约 40 个读函数**(`GetInterface` ×2、`GetInterfaceCount`、`Draw3dSolid`、`ImpGetGripPoints`、`GetSubPrimObj*`、`ActiveInterface`、`NozzhasFlage`、13 个 `Draw*2d`、`PrimPorjectToPlan`、`ToJson` …),每一处都要在开头调 `EnsureSubDMeshParsed()`,漏一个就是「少管嘴 / 少几何 / 夹点错位」
2. **索引语义会变**:`GetSubPrimObj(n)`、`SetActiveSubPrimObj(n)`、`RemoveSubPrimObj(n)` 的下标在解析前后指向不同单元。用户如果在解析前拿到下标,解析后再用,就删错/选错
3. `copyFrom` / `ByJson` 拷的是「当前数组」,源对象没解析时目标也没解析,需要在拷贝时正确传递标志
4. 收益不确定:`AcDb` 开图后可能立刻调 `subGetGeomExtents`,那样惰性化等于没省
**建议的替代路径**(按优先级):
1. 先量:在真实图纸上统计「开图总耗时」与 `subDMesh2NewSolid` 的累计耗时。占比不到 20% 就不值得动
2. 若确实是大头,优先做「解析结果缓存」:以 `m_pData` 内容的 hash 为 key 缓存解析出的 `XPrimData` 数组,第二次遇到同一个族直接深拷贝。语义完全不变,风险最低
3. 最后才考虑惰性化,且必须把所有读入口一次改全
**2026-10-25 决定不做**:三步里的前两步(先量、解析缓存)都要先有真实图纸的耗时数据支撑,而这一步的改动面(`dwgInFields` + `subDMesh2NewSolid` + 可能的解析缓存)远大于收益的不确定性,暂不投入。等实际有用户反馈"开图慢"、并且能量到 `subDMesh2NewSolid` 占比超过 20% 时再回头做。
### 4.7 度量建议
| 指标 | 位置 | 目的 |
| --- | --- | --- |
| `Draw3dSolid(XPrimData)` 调用次数 / 累计耗时 | `:3526` | 确认造体占比 |
| `booleanOper` 次数 / 累计耗时 | `:3867` `:3964` | 确认布尔占比 |
| 3D 缓存长度峰值 | `Draw3d:3036` | 验证 4.2 的膨胀 |
| `GetBodyBox` 触发的 `Draw3d` 次数 | `:4205` | 验证 4.3 |
| `GetDraw2dables` 建块次数 | `:2909` | 验证 4.5 |
| 开图总时长 / `subDMesh2NewSolid` 时长 | `:1791` | 验证 4.6 |
场景:单设备拖拽夹点、全图缩放、图层冻结后重生成、剖切、2D 导出。
---
## 5. 优化方案(分批)
### 5.1 第一批:内存与正确性(不改接口、不改格式)
分两批提交:**内存问题已完成(见 2.13)**,**逻辑缺陷已完成(见 3.2~3.14)**。
| # | 改动 | 风险 | 状态 |
| --- | --- | --- | --- |
| 1 | 删掉 `dwgInFields` 里的重复分配,`readString` 补 `acutDelString` | 低 | 已完成 |
| 2 | 析构补 `FreeSubPrimData()` | 低 | 已完成 |
| 3 | `releaseData()` 统一 `delete[]`,`FreeSubPrimData()` 释放 `m_pData` 后删结构体 | 低 | 已完成(见 2.3、2.13) |
| 4 | `subDMesh2NewSolid` 按下标倒序删除并释放 | 中 | 已完成 |
| 5 | `RemoveSubPrimObj*` 补释放 `m_pData` | 低 | 已完成 |
| 6 | `subErase` 的 `Find` 补 `!= -1` | 低(行为会变) | 已完成 |
| 7 | `DrawHandFlg` 的 `==` 改 `=` | 低 | 已完成 |
| 8 | `ProcessHlrResult` 改用 `while` + `erase(it++)` | 低 | 已完成 |
| 9 | `copyFrom` 补 `m_bVert` / `m_idDash` / `m_idCenter` | 低 | 已完成 |
| 10 | `GetSectionDraw` 返回值改 `nCoundPre - nCount` | 低 | 已完成 |
| 11 | `clearGeoBuffer` 实现为清 2D/3D 缓存 | 中(影响所有改参数路径) | 已完成 |
| 12 | `MakeBlockRefSet` 范围累加改判 `extAdd` | 低 | 已完成 |
| 13 | `byJson` 按字节长度分配 `m_pData` | 低 | 已完成 |
| 14 | `dwgInFieldsForXPrim` 加尺寸上限与读失败处理 | 低 | 已完成 |
| 15 | `GetBodyBox` 返回实际结果 | 低 | 已完成 |
| 16 | `GetInterface` 数组版补 `dUnit` | 低(仅米制图纸有差异) | 已完成 |
另外补做的 4 处内存修复(2.6、2.7、2.9、2.10、2.11、`GetDraw3dables` 合并泄漏、`dwgInFields` 的 `RemoveAll`)见 2.13。
### 5.2 第二批:`m_Combins` 存盘清空(已完成)
| 步骤 | 内容 | 状态 |
| --- | --- | --- |
| 1 | 确认 `kCombination` 的使用:`EquipmentMakeDlg.cpp:4357` 创建时填充,`toJson:789` / `Draw3dSolid:3844` 递归使用 | 已确认在用 |
| 2 | 删掉 `:279` 的恒真条件与 `m_Combins.clear()`,按实际有效数量写出 | 已完成(PR #48) |
| 3 | 不做读入容错:写端正确后不存在"非组合类型带子单元"的数据,加过滤反而可能误删 | 已取消 |
| 4 | 用含组合设备的样例做"存盘→重开→几何完整"验证 | 待实测 |
没有按原计划"修成 `&&`":`m_eType` 判断本身是多余的,`m_Combins` 只有组合单元才会非空;保留判断等于把正确性押在枚举比较上,和原来的错误同源。也没有数据迁移——受影响的图纸里组合设备的子单元已经被写没了,无法从 dwg 恢复,只能重建。
### 5.3 第三批:性能(先量后改)
| 优先级 | 改动 | 预期 | 状态 |
| --- | --- | --- | --- |
| 高 | 拖拽时走缓存,不重建(4.2) | 拖拽帧率、内存峰值 | 已完成(PR #53) |
| 高 | 2D 路径改用参数 bbox,不触发 3D(4.3) | 一次性开销,收益有限 | 暂缓 |
| 中 | 单元矩阵复用,去掉临时实体(4.4) | 夹点、osnap | 不做,收益不明显 |
| 中 | `GetDraw2dables` 不建块(4.5) | 导出、DWG 体积 | 暂缓,调用方未确认 |
| 低 | `subDMesh2NewSolid` 惰性化(4.6) | 开图 | 不做,改动大、收益不确定 |
`INSUNITS` 缓存(4.4 的第一条)已决定不做。
性能部分到此收尾:只有 4.2(拖拽走缓存)落地,其余全部按上面的结论维持现状。
### 5.4 需要先确认的事
1. ~~`booleanOper` 是否删除入参(3.15)~~ —— 2026-10-23 按「不删除入参」处理,理由见 3.15
2. ~~`GetSectionDraw` 的返回值调用方到底期望什么(3.5)~~ —— 2026-10-25 核实全库调用点都忽略返回值,已按「返回新增个数」修正
3. ~~`kCombination` 单元的实际存量(5.2)~~ —— 2026-10-25 确认仍在用,见 3.1
4. ~~`PreprocessForHlr` 的 `mat4` 双次变换是否刻意(3.12)~~ —— 2026-10-25 确认是故意的:`mat3` 移动半个设备高度,两次正好一个设备高度,不改
5. `HLR_CALC` 是否从 `m_mapSpecXData` 的序列化里排除(3.6):只有在剖切过程中出意外(保存、崩溃、取消)才会写进 dwg,目前没碰到,先放一放
6. ~~`GetInterface` 数组版是否补 `dUnit`(3.7)~~ —— 2026-10-25 确认是 bug,已补;数组版内部 4 处调用不消费 `dSize`,外部只有 PCF 设备导出受影响,见 3.7
7. `AecDbEwFitting.cpp:16523` / `AecDbECFitting.cpp:8751` 的 `GetSectionDraw` 同样写反,是否一起改
---
## 6. 风险与验证清单
| 改动 | 验证方式 |
| --- | --- |
| 夹点数组 / 析构释放 | 打开含 1000+ 设备的图纸,任务管理器看私有字节;关图后应回落 |
| `releaseData` 统一 `delete[]` | 各类单元(box/cylinder/cone/Revit/组合)各删一个,确认无崩溃;开图→改参数→存盘→重开,几何完整 |
| `m_Combins` 存盘 | 新建组合设备 → 存盘 → 关图 → 重开,组合几何完整;再存盘一次仍完整(验证不是"只活一次") |
| 剖切 / 2D 导出 | 对含阀门、法兰、普通设备的图各剖一次,反复开关剖切对话框,内存不持续上涨 |
| `subDMesh2NewSolid` | 含 SubDMesh 的样例图,开图后单元数量与类型正确,绘制不缺体 |
| `subErase` | 删除阀门/接头/普通设备各一次,确认只有阀门走合并管线 |
| `HLR_CALC` | 剖切过程中保存再重开,设备 3D 仍可见 |
| `copyFrom` | 复制粘贴竖放阀门,方向与图层保持 |
| `DrawHandFlg` | 液动阀(驱动类型 3)2D 符号旁显示 `Y` |
| `ProcessHlrResult` | 反复开关阀门剖切 2D 显示,不崩溃 |
| `clearGeoBuffer` | 改单元参数(box/cylinder/cone)后关闭对话框,几何立即更新 |
| `MakeBlockRefSet` | 剖切/2D 导出生成的块,插入点位于设备中心而不是原点 |
| `byJson` | JSON 导入设备,几何与参数正确 |
| `subErase` 的 `Find` | 删除普通设备后,相邻管线不被合并 |
| `GetInterface` 单位 | 米制图纸(`INSUNITS` = 6)里插入设备,接口尺寸与下标版一致;PCF 导出的管径不再大 1000 倍 |
| 拖拽优化 | 拖夹点 10 秒,内存不涨、帧率稳定;松手后几何位置正确 |
| 2D bbox | 各阀门 2D 符号方向、手柄位置与改前一致 |
本仓库没有单元测试,验证只能靠"编译 + CAD 内实测"。编译范围按既有约定:Debug + R190,只编 `AecRxBase`、`CoreLibrary_BAL_REMOTE`、`CoreCommand_BAL_REMOTE`、`CoreImplement`。
---
## 7. 明确不做
- 不改 `XPrimData` 的存储布局和 `kCurrentVersionNumber`(会带来新老插件不兼容,与 `AcDbX3dSolid` 阶段三取消的理由一致)
- 不把 2D 符号系统(25 个 `Draw*2d`)重构成数据驱动,收益不明确、回归面太大
- 不动 `AecDbEwFitting` / `AecDbHvacFitting` 等同类实体,除非确认它们的同一批写法也要一起改
---
## 8. 实施顺序
1. ~~第一批 1~5(内存),单独一个 PR,可独立验证~~ —— 已完成,范围扩到 2.1~2.11 全部内存项,见 2.13
2. ~~第二批(`m_Combins` 存盘清空)~~ —— 已完成(PR #48),见 3.1、5.2
3. ~~第一批 6~11(逻辑)~~ —— 已完成,范围扩到 3.2~3.14 共 10 项,见 5.1
4. ~~确认 5.4 剩下的问题~~ —— 3.7 已改(见 3.7),3.12 确认是故意的、不改,3.6 暂缓
5. ~~第三批(性能)~~ —— 4.2 已完成(PR #53,用户自行修改并验证);4.3 / 4.5 暂缓,4.4 / 4.6 不做,性能部分收尾