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

19 KiB
Raw Blame History

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 只写类名,不写参数

// 写端 :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:

// 面积最大的当外环
if (dAreaTmp > dArea) { dArea = dAreaTmp; pRegionBig = pRegion; }
// 其余全部减去
es = pRegionBig->booleanOper(AcDb::kBoolSubtract, pRegion);

createFromCurves 对 N 条闭合曲线会生成 N 个 region。截面是两个不相交的独立区域时,小的那块被当成孔减掉。

(4) 退化判据不成立

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:

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:

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)

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)

// 只取文件名,不接受 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)

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)

// 现在:两个都无效才跳过
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(...)。

es = rawBooleanOperWrapper(pRegionBig, pRegion, AcDb::kBoolSubtract);
if (es != Acad::eOk)
{
    acutPrintf(_T("\nAecDbEwPipeBase: 截面布尔失败,err=%d。"), es);
}

(3) spline 读端补日志

读端 :562 的 spline 分支现在什么都不做,直接落到 return NULL。至少加一行日志,让老图纸里丢失的边能被发现:

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() 同思路:

// 头文件
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,后续所有改动都只影响读路径,风险可控。