Files
envi-code/AecDbEwPipeBase-region-sat-plan.md
T
company-desktop-computer 5692d9010b docs: 补充 AecDbEwPipeBase 截面存 SAT 的落盘实测与不分流决策
- 新增 3.7 落盘实测:SAT 原始字节、DWG 净增、acisIn 与布尔重建耗时,
  并标注该数据来自 accoreconsole 模拟 REGION,非真实实体实测
- 新增 3.8 为什么不分流:createFromCurves 慢、落盘省不下体积、
  写路径缺陷、升版本成本
- 修正体积代价表述:字段级估算 5~20 倍,落盘压缩后实为 4~5 倍
2026-10-21 22:04:16 +08:00

468 lines
19 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.
# AecDbEwPipeBase::m_pRegion 存储改为 SAT 为主
> 目标文件:`SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp`、`inc/AecDbEwPipeBase.h`
> 当前版本号:`kCurrentVersionNumber = 7`(`AecDbEwPipeBase.cpp:382`)
> 本文只讨论 `m_pRegion`(截面)的持久化,不涉及 `m_pBaseCurve`、`m_arUserInfs`。
## 1. 结论
`m_pRegion` 的存储改为 **SAT 为主、曲线通道只读兼容**。
- 写入:统一走 `nLen = 0` + `RegionAcisoutDwgOutFields`(SAT 字节流)
- 读取:保留两条通道,`nLen > 0` 走曲线重建,`nLen == 0` 走 SAT
- 曲线通道的**写路径退役**,读路径永久保留(老图纸需要)
- **不按截面类型分流**:不存在「标准截面走参数重建、异形截面走 SAT」的第二套逻辑,所有新数据一律 SAT。理由见 3.8
体积代价经实测确认可接受(落盘净增约 4 倍,见 3.7);换来的是几何可重现、失败可检测、代码只有一套。
---
## 2. 现状
### 2.1 双通道结构
```
dwgOutFields (:1324)
└ SectionRegionDwgOutFields(ver=7) (:1230)
├ m_pRegion->explode() 非空 → RegionExplodeDwgOutFields (:1135)
│ └ writeUInt32(曲线数) + 每条曲线(类名 + 参数)
└ explode 空 → writeUInt32(0) + RegionAcisoutDwgOutFields (:1150)
└ acisOut → 读临时文件 → writeString(文件名) + writeUInt32(nLen) + writeBytes(SAT)
dwgInFields (:854)
└ m_pRegion = SectionRegionDwgInFields(ver) (:893, :825)
├ ver < 4 → readUInt32(nLen) → RegionFromCurvesDwgInFields (:663)
└ ver >= 4
├ nLen > 0 → RegionFromCurvesDwgInFields
└ nLen == 0 → RegionAcisInDwgInFields (:758)
```
曲线通道支持的类型:`AcDbLine` / `AcDbArc` / `AcDbCircle` / `AcDbPolyline`(含 bulge 与宽度)/ `AcDbEllipse` / `AcDb3dPolyline` / `AecDb3dRoadAxisLine`(`BaseCurveDwgOutFields:1013`、`BaseCurveDwgInFields:445`)。
### 2.2 曲线通道的四个缺陷
**(1) `AcDbSpline` 只写类名,不写参数**
```cpp
// 写端 :1092
AcDbSpline* pSp = AcDbSpline::cast(pBaseCurve);
if (pSp != NULL)
{
//...
return Acad::eOk; // 一个参数都没写
}
// 读端 :562
AcDbSpline* pSp = AcDbSpline::cast(pBaseCurve);
if (pSp != NULL)
{
//... // 不读参数,也不 return
}
// 继续 cast 到 3dPolyline / RoadAxisLine 全部失败,最终 :596 return NULL
```
读写对称,所以不会字段错位,但**这条边直接消失**。而 `AcDbRegion::explode()` 对含 NURBS 边的 region 返回 `AcDbSpline` 是常见情况。
**(2) `explode` 的输出依赖 ACIS 版本**
`explode` 是从 B-rep 反推边界,NURBS 边被转成 spline 或打碎成多段线段,结果随 ACIS 版本变化。同一份截面在 CAD 2023 和 2027 上存盘,写出的字节不同;来回存盘几何会逐步漂移。
**(3) 多区域靠"最大面积为外环"猜**
`:717-751`:
```cpp
// 面积最大的当外环
if (dAreaTmp > dArea) { dArea = dAreaTmp; pRegionBig = pRegion; }
// 其余全部减去
es = pRegionBig->booleanOper(AcDb::kBoolSubtract, pRegion);
```
`createFromCurves` 对 N 条闭合曲线会生成 N 个 region。截面是两个不相交的独立区域时,小的那块被当成孔减掉。
**(4) 退化判据不成立**
```cpp
m_pRegion->explode(arCurPtrs);
if (!arCurPtrs.isEmpty()) // 只看"有没有炸出东西"
return RegionExplodeDwgOutFields(...);
```
上面三种情况(spline 丢失、多区域误判、边被打碎)都会让 `arCurPtrs` 非空,于是**走曲线通道、写出坏数据、读回来几何不对,没有任何提示**。
### 2.3 SAT 通道自身的缺陷
| 位置 | 问题 |
| --- | --- |
| `:775` | `new char[nLen]` 无 `+1`,当前没有 strlen 依赖,但脆弱 |
| `:784` | `errorfile` 分支 `return pRegion` 前没有 `delete[] pszTxt`,每次泄漏 nLen 字节 |
| `:788-790` | 临时文件路径由 DWG 里的 `sFileName` 拼成,可被构造成 `..\..\xxx` |
| `:1162` | 文件名 `Region_<句柄>.dat` 同图不同文档会冲突 |
| `:1182` | `new char[nLen]` 无 `+1`,`writeBytes` 有长度,当前安全 |
| `:1206` | `errorfile` 兜底走 `m_pRegion->dwgOutFields(pFiler)`,只写字段不写对象头,`layerId` 等为 `kNull` |
| 全路径 | 失败无日志,`return NULL` 后 `m_pRegion` 为空,绘制时截面直接不见 |
---
## 3. 改为 SAT 为主的优点
### 3.1 几何可重现
SAT 是 ACIS 自己的序列化格式,`acisOut` / `acisIn` 是同版本内的官方往返。同一份 `m_pRegion`,无论在哪台机器、哪个 ACIS 版本上存盘,写出的字节一致(同版本内)。
曲线通道相反:`explode` 的输出随版本变化,同一张图反复存盘会累积漂移。
**这是最重要的一条**:图纸数据一旦不可重现,问题就永远无法复现和定位。
### 3.2 失败可检测
SAT 首行带 ACIS 版本号和 data record 数,`fixACISFormat` 能用它校验完整性(`CoreTools.cpp`)。曲线通道没有整体校验:少一条边、多一段线,格式上完全合法,无法发现。
配合 `AcDbX3dSolid` 那次加的失败日志(`CoreTools::StringToRegion`),解析失败至少能在命令行看到长度和首行。
### 3.3 代码只有一套
写路径删掉 `explode` + `RegionExplodeDwgOutFields` + `BaseCurveDwgOutFields` 的调用后:
- 不需要为每种曲线类型维护写/读分支
- 不需要维护"最大面积为外环"的启发式
- 不需要维护 spline 的补全逻辑
- 与 `AcDbX3dSolid` 的存储方式一致,后续只有一个心智模型
`BaseCurveDwgOutFields` / `BaseCurveDwgInFields` 仍要保留,因为 `m_pBaseCurve`(中心路径)用它,且曲线读通道依赖它。
### 3.4 写盘更快
`SectionRegionDwgOutFields` 现在每次存盘对每个管道做一次 `m_pRegion->explode()`,这是 ACIS 边界提取,成本与 `acisOut` 同量级甚至更高(要构造曲线对象并写多个字段)。改成 SAT 后只剩一次 `acisOut`。
另外 `explode` 出来的曲线在 `RegionExplodeDwgOutFields:1145` 用 `delete pCurve` 释放,未入库实体没有先 `close()`,属于隐患。
### 3.5 格式已预留,切换无版本成本
读取端 `SectionRegionDwgInFields:836-849`:
```cpp
readUInt32(&nLen);
if (nLen > 0) → 曲线通道
else → SAT 通道
```
只要写入时固定写 `nLen = 0`,**旧版本插件读到新数据也会走 SAT 分支**,能正确还原。
这意味着:
- 不需要升 `kCurrentVersionNumber`
- 新图纸在老版本 CAD 上不会变 proxy
- 老图纸(`nLen > 0`)继续走曲线通道
对比 `AcDbX3dSolid`:它没有预留位,改成 `m_pSatBuffer` 必须升版本到 5,老插件会 `eMakeMeProxy`。
### 3.6 代价:体积(字段级估算)
| 截面 | 曲线通道 | SAT 通道 |
| --- | --- | --- |
| 矩形(4 条 Line) | ~370 B | 2~4 KB |
| 圆形(1 个 Circle) | ~80 B | 2~3 KB |
| 20 孔异形 | ~2 KB | 20~50 KB |
曲线通道的固定开销:每条曲线 `writeString(类名)` ≈ 24 B + `Point3d` × 2 = 48 B + `Vector3d` = 24 B ≈ 92 B。
按字段级估算,未压缩的 SAT 比曲线通道大 5~20 倍。但 DWG 保存时会对整图做压缩,实际落盘差距远小于此(见 3.7),而且复杂截面(spline 被打碎成数百段)时曲线通道可能反超 SAT。
**结论:用体积换正确性。** 在图纸数据上这个交换是值得的。
### 3.7 落盘实测
> **测量方式**:`accoreconsole` 脚本直接造原生 `REGION` / `SUBTRACT`,**未加载本插件、未创建 `AecDbEwPipeBase` 实例**。
> 因此下表只反映「截面数据本身的编码体积与重建耗时」,不含实体其它字段,也不能直接乘出整张图纸的增量。
**原始 SAT 大小**(`acisOut` 落盘的 ASCII 文本,未压缩):
| 截面 | 边数 | SAT 字节 | 拟合 |
| --- | --- | --- | --- |
| 圆环 | 2 | 1138 | ≈ 200 + 420 × 边数 |
| 矩形 | 4 | 1548 | 同上 |
| 矩形 + 2 孔 | 6 | 2232 | 同上 |
| 20 边形 | 20 | 8822 | 同上 |
**DWG 落盘净增**(同一张图,500 个实体减 100 个实体,差值 ÷ 400):
| 通道 | 每实体净增 |
| --- | --- |
| 曲线通道 | ~32 B |
| SAT 通道 | 126~160 B(其中 SAT 文本 ~112 B) |
即:**压缩后 SAT 只比曲线通道大 4~5 倍**,而不是 3.6 表里的 5~20 倍。SAT 是高度结构化的重复 ASCII,DWG 的压缩对它的效果远好于对曲线二进制记录。
**重建耗时**:
| 方式 | 单次耗时 |
| --- | --- |
| `acisIn`(SAT 通道) | 1.88 ms |
| 2 × REGION + `SUBTRACT`(曲线通道) | 3.59 ms |
SAT 约快 1.9 倍。
**数据可信边界**:
- 可信:字节随边数的增长关系、两条通道的落盘相对量级、`acisIn` 与布尔运算的耗时比
- 不可外推:具体图纸的总体积增量。真实增量 = 每实体截面数 × 截面复杂度 × 上述单值,且实体自身字段也在压缩
- 待补:在装了 Envi3D 的 CAD 里,用真实管道实体做一次「新建 N 个 → 存盘 → 对比」的验证
### 3.8 为什么不分流
曾考虑过「标准截面(矩形、圆)走参数重建,异形截面走 SAT」,被否掉:
1. **曲线通道要 `createFromCurves`**:即使曲线是精确参数(不带 spline),重建仍要跑 ACIS 的边界构造 + 多环布尔,实测 3.59 ms/次,比 `acisIn` 慢 1.9 倍。开图时每个管道都要付一次
2. **体积省不下来**:落盘压缩后曲线通道只有 ~32 B/实体,省的是 100 B 量级,不值得为它多维护一条分支
3. **两条写路径 = 两份 bug**:现有曲线通道的缺陷(写端 spline 只写类名不写参数、读端 `&&`/`||` 写反、布尔未做 SEH 保护)都集中在「曲线 ↔ 几何」这条转换链上。读路径因为老图纸必须留着,但写路径一旦也留着,就会有新数据持续进入这条有缺陷的链
4. **版本保护成本**:分流需要升 `kCurrentVersionNumber` 到 8,新图在老 CAD 上变 proxy;统一 SAT 用 `nLen == 0` 就够,不用升版本
结论:**一条写路径,SAT 到底。** 曲线通道只作为老图纸的只读兼容层存在。
---
## 4. 优化方案
### 4.1 第一步:写路径切换
改 `SectionRegionDwgOutFields:1230`:
```cpp
Acad::ErrorStatus AecDbEwPipeBase::SectionRegionDwgOutFields(AcDbDwgFiler* pFiler, int nVer) const
{
assertReadEnabled();
if (m_pRegion == NULL)
return Acad::eOk;
// 统一写 SAT:nLen = 0 是读取端的 SAT 分支标志,旧版本插件同样识别
// 曲线通道仅用于读取老图纸,不再产生新数据
Adesk::UInt32 nCurs = 0;
pFiler->writeUInt32(nCurs);
return RegionAcisoutDwgOutFields(pFiler);
}
```
`dwgOutFields:1348-1364` 的 `m_pRegion == NULL` 分支保持不变(写 0 + `"***"` + 7 字节占位),读取端走 `RegionAcisInDwgInFields`,读到 `"***"` 后 `acisIn` 失败返回 NULL,行为与现在一致。
**这一步完成后曲线通道的写路径已经不再执行**,`RegionExplodeDwgOutFields` 只剩读路径依赖的 `BaseCurveDwgOutFields`。
### 4.2 第二步:修 SAT 通道自身缺陷
按优先级:
**(1) `errorfile` 分支内存泄漏(`:775-785`)**
```cpp
char* pszTxt = new char[nLen + 1];
::memset(pszTxt, 0, nLen + 1);
pFiler->readBytes(pszTxt, nLen);
if (sFileName.CompareNoCase(_T("errorfile")) == 0)
{
AcDbRegion* pRegion = (AcDbRegion*)AcDbRegion::desc()->create();
Acad::ErrorStatus es = pRegion->dwgInFields(pFiler);
delete[] pszTxt; // 补上
if (es != Acad::eOk) { delete pRegion; return NULL; }
return pRegion;
}
```
**(2) 临时文件路径收敛(`:788-790`)**
```cpp
// 只取文件名,不接受 DWG 里的路径成分
CString sName = sFileName;
int nSlash = max(sName.ReverseFind(_T('\\')), sName.ReverseFind(_T('/')));
if (nSlash >= 0)
sName = sName.Mid(nSlash + 1);
if (sName.Find(_T("..")) >= 0)
sName = _T("region_tmp.dat");
CString sFullPath = FilePath::getSystemTemporaryFolder();
sFullPath += _T("\\");
sFullPath += sName;
```
**(3) 写端文件名加进程号(`:1162`)**
```cpp
sFileName.Format(_T("Region_%lu_%s.dat"), (unsigned long)::GetCurrentProcessId(), szHandleStr);
```
读端从 DWG 里读到的就是带进程号的名字,写到 temp 时再拼回去。这样同图在两个 CAD 实例里同时打开不会互相覆盖。
注意:这只解决"同时打开",不解决"同一进程内两个文档"。若需要彻底解决,可把句柄 + 文档名(`acdbHostApplicationServices()->workingDatabase()` 的路径哈希)一起编进文件名。
**(4) 失败日志**
`RegionAcisInDwgInFields` 的每个失败分支加 `acutPrintf`,带 `nLen`、文件名、`acisIn` 返回值,与 `AcDbX3dSolid` 的日志格式保持一致。
**(5) `new char[nLen]` 补 `+1`**
`:775` 和 `:1182` 都补 `+1`,即使当前没有 strlen 依赖,也避免以后被当字符串用时越界。
### 4.3 第三步:读路径的曲线通道加固
老图纸必须继续可读,`RegionFromCurvesDwgInFields:663` 的三处要修:
**(1) `isRegionNull` 的判断条件写反了(`:745`)**
```cpp
// 现在:两个都无效才跳过
if (isRegionNull(pRegion) && isRegionNull(pRegionBig))
continue;
// 应该:任意一个无效就跳过
if (isRegionNull(pRegion) || isRegionNull(pRegionBig))
continue;
```
`isRegionNull` 返回的是 `getAreaProp` 失败(`:640-661`),一个无效一个有效时进 `booleanOper` 正是最容易崩的组合。
**(2) `booleanOper` 没走 SEH 保护(`:750`)**
同文件 `:600` 已有 `rawBooleanOperWrapper`,用 `__try/__except` 捕 `0xC0000005`,说明 ACIS 布尔会崩。这里却直接调 `pRegionBig->booleanOper(...)`。
```cpp
es = rawBooleanOperWrapper(pRegionBig, pRegion, AcDb::kBoolSubtract);
if (es != Acad::eOk)
{
acutPrintf(_T("\nAecDbEwPipeBase: 截面布尔失败,err=%d。"), es);
}
```
**(3) spline 读端补日志**
读端 `:562` 的 spline 分支现在什么都不做,直接落到 `return NULL`。至少加一行日志,让老图纸里丢失的边能被发现:
```cpp
AcDbSpline* pSp = AcDbSpline::cast(pBaseCurve);
if (pSp != NULL)
{
acutPrintf(_T("\nAecDbEwPipeBase: 截面边界含 spline,历史数据未保存参数,该边将被丢弃。"));
delete pBaseCurve;
return NULL;
}
```
不要试图"补全" spline 参数:历史数据里本来就没写,读端无从恢复。写端已经不再产生新数据,这条路径只服务老图纸。
### 4.4 第四步(可选):`m_pRegion` 懒加载
`dwgInFields:893` 现在无条件 `m_pRegion = SectionRegionDwgInFields(...)`,每个管道实体开图时都要跑一次 `createFromCurves`(ACIS 布尔)或 `acisIn`。
与 `AcDbX3dSolid` 的 `EnsureCreated()` 同思路:
```cpp
// 头文件
mutable bool m_bNeedRegion;
AcDbRegion* EnsureRegion() const; // 内部 const_cast 调 ReCreateRegion
// dwgInFields
m_bNeedRegion = true; // 不立即重建
// 所有读取 m_pRegion 的入口
AcDbRegion* AecDbEwPipeBase::GetSection() const
{
if (m_bNeedRegion)
{
m_bNeedRegion = false;
const_cast<AecDbEwPipeBase*>(this)->ReCreateRegion();
}
return m_pRegion;
}
```
**风险**:`m_pRegion` 在类内被直接访问 20+ 处(`:1539`、`:2759`、`:3104`、`:3358`、`:3898`、`:3910` 等),没有统一的 getter。收口工作量大,漏一个就是空指针或空截面。
建议放在写路径切换稳定之后再单独做,不要和格式切换混在一个 PR。
### 4.5 第五步(可选):曲线读路径的最终收敛
如果确认所有在用图纸的 `nLen > 0` 数据已经过时,可以考虑:
- 读到时自动升级:`nLen > 0` → 用曲线重建 → 下次存盘写成 SAT
- 由于 4.1 的写路径已经固定写 SAT,这一步**不需要额外代码**,打开再保存即完成迁移
`RegionFromCurvesDwgInFields` 和 `BaseCurveDwgInFields` 仍需保留(`m_pBaseCurve` 也用后者),不能删。
---
## 5. 兼容性
| 场景 | 行为 |
| --- | --- |
| 老图纸 + 新插件 | `nLen > 0` → 曲线通道,行为不变 |
| 新图纸 + 新插件 | `nLen == 0` → SAT 通道 |
| 新图纸 + 旧插件(v4+) | `nLen == 0` → SAT 通道,可正常还原 |
| 新图纸 + 旧插件(v4 以下) | 不支持,与现状一致(v4 以下本来就只认曲线通道) |
**不升版本号**,`kCurrentVersionNumber` 保持 7。
---
## 6. 风险
| 风险 | 影响 | 对策 |
| --- | --- | --- |
| 落盘体积增大约 4 倍(每实体 +100 B 量级) | 大图纸体积略增 | 实测可接受(3.7);不需要自定义压缩 |
| `acisOut` 对某些退化 region 失败 | 截面丢失 | 失败时保留 `errorfile` 兜底,并加日志 |
| 老图纸里的曲线数据本身有损(spline 丢失) | 重建后的截面与原始不同 | 属于历史数据问题,本次不引入;加日志让问题可见 |
| 4.4 懒加载收口遗漏 | 空指针 | 单独 PR,逐处核对 |
---
## 7. 验证
### 7.1 编译
按项目约定:`R190` + `Debug`,目标 `AecRxBase`、`CoreLibrary_BAL_REMOTE`/`_TRIAL_YEAR`、`CoreCommand_BAL_REMOTE`/`_TRIAL_YEAR`、`CoreImplement`。
### 7.2 CAD 实测
1. **新建管道 → 存盘 → 重开**:截面几何、面积、位置一致
2. **老图纸**:改动前保存的图,打开后截面正确(走曲线通道)
3. **老图纸另存**:打开后直接保存,再重开,截面仍正确(已转成 SAT)
4. **混合**:同一张图里既有老数据又有新建管道,存盘重开后都正确
5. **复制粘贴**:`copyFrom`(`:1539`)走 `m_pRegion->clone()`,验证不丢截面
6. **布尔截面**:带孔的异形截面(多曲线)在曲线通道下的重建结果与 SAT 通道一致
### 7.3 指标
已在 3.7 用模拟截面测过「单截面 SAT 字节 / 落盘净增 / `acisIn` 耗时」。仍需在真实数据上补:
- 同一张典型图改动前后的 DWG 体积(含全部实体字段)
- 存盘耗时(对比 `explode` 与 `acisOut`)
- 开图耗时(若做了 4.4)
---
## 8. 明确不做
| 项 | 原因 |
| --- | --- |
| 删除 `RegionFromCurvesDwgInFields` | 老图纸依赖 |
| 删除 `BaseCurveDwgInFields` / `BaseCurveDwgOutFields` | `m_pBaseCurve` 在用 |
| 补全 spline 的读写参数 | 历史数据里没有,补了也读不回来;写路径已退役 |
| 升 `kCurrentVersionNumber` | 格式已预留,不需要 |
| 自动迁移老图纸 | 打开再保存即完成迁移,不需要额外代码 |
| 给 `AcDbX3dSolid` 增加曲线通道 | 拿不到构建曲线,且曲线通道有上述缺陷 |
---
## 9. 实施顺序
| 步骤 | 内容 | 风险 | 独立 PR |
| --- | --- | --- | --- |
| 1 | 写路径切换(4.1) | 低 | 是 |
| 2 | SAT 通道缺陷修复(4.2) | 低 | 可合并到 1 |
| 3 | 曲线读路径加固(4.3) | 低 | 是 |
| 4 | 懒加载(4.4) | 中 | 是 |
| 5 | 真实管道实测(3.7 待补项) | — | — |
第 1 步落地后,新产生的数据全部是 SAT,后续所有改动都只影响读路径,风险可控。