Files
envi-code/AcDbX3dSolid-refactor-plan.md
T
company-desktop-computer bed6556adf docs: 明确阶段三单向兼容——老版看不到新图 region 几何
兼容方案定为第一项:不升 kCurrentVersionNumber,新版能完整读旧版图纸,
旧版打开新图时 region 类型的 AcDbX3dSolid 几何不可见。

补充说明 m_pSatBuffer 在旧版也会原样读写,所以旧版另存后回新版仍能恢复;
非 region 类型(点串、SubDMesh、管廊脚本)不受影响。

文档开头的原则、4.3.4 的不升版本理由同步。
2026-10-22 10:43:45 +08:00

406 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.
# AcDbX3dSolid 重构计划
> 目标:解决 `AcDbX3dSolid` 用 `CString` 保存几何数据(`m_sScr` / `m_sszPoint2ds`)带来的体积翻倍、精度丢失、临时文件冲突和解析开销问题。
>
> 范围:`SourceCode/Code2026/CoreLibrary/AcDbX3dSolid.cpp`(5596 行)、`inc/AcDbX3dSolid.h`、`CoreCommand/CoreTools.cpp`。
>
> 原则:**单向兼容——新版必须能读老图纸,老版不保证能显示新图的 region 类型几何**(见 11.1);写入统一新格式;不改对象模型(`AcDbX3dSolid` 仍是可平凡复制的数据类)。
---
## 1. 现状
`AcDbX3dSolid` 不是 `AcDbEntity`,是"参数化几何单元",只存生成参数,按需重建 `AcDb3dSolid`。
两个 `CString` 承担了超出设计的职责:
| 字段 | 实际内容 |
| --- | --- |
| `m_sszPoint2ds` | 断面/路径点串、引导线、GSMarker 十进制整数 |
| `m_sScr` | **ACIS SAT 全文**(region 类,9 种类型)、管廊断面脚本(`kEarthWorm` / `kSHEarthWorm`) |
region 类的数据流:
```
AcDbRegion* ──RegionToString()──► %TEMP%\Region1.sat ──读文件──► char* ──ANSI→UTF-16──► CString m_sScr
│
dwgOutFields: writeString (2 字节/字符)
```
读取反向:`CString` → UTF-8 → `fixACISFormat` → 写 `%TEMP%\Region2.sat` → `acisIn`。
已知问题:
| # | 问题 | 位置 |
| --- | --- | --- |
| 1 | SAT 是 ASCII,却按 UTF-16 存 DWG,**体积翻倍** | `:4783` `writeString((LPCTSTR)m_sScr)` |
| 2 | 临时文件名写死(`Region1.sat` / `Region2.sat` / `Region212.sat`),多 CAD 实例并发会互相覆盖 | `CoreCommand/CoreTools.cpp` |
| 3 | `new char[dwFileLength]` 缺 `+1`,无结尾零字节,`CString` 赋值时越界读;`file.Read` 返回值未检查 | `CoreTools::RegionToString` |
| 4 | `dwgInFields` 末尾无条件 `ReCreate()`,**开图时每个单元一次临时文件往返** | `:4753` |
| 5 | 分隔符 `\|`、`@`、`@region@` 无转义、无校验 | 多处 |
| 6 | `Clone()` 内含 `ReCreate()`,绘制缓存建立时重复重建 | `:5593` + `:295` |
---
## 2. 已否决的方案
| 方案 | 否决原因 |
| --- | --- |
| **存构建曲线 + `AcDbRegion::createFromCurves`** | 接口只拿得到 `AcDbRegion*`,可能是复杂布尔结果;`AcDbRegion` 无反向导出边界 API(`dbregion.h` 只有 `getPerimeter`/`getArea`/`getPlane`/`getAreaProp`/`booleanOper`);`explode` 得到的是 ACIS 求交后的边,是近似 |
| **自写 / 复用 `AcDbDwgFiler` 做内存序列化** | R26 `readString(ACHAR**)` 已 `final`(`arx/R26.0/inc/dbfiler.h:148`),基类实现转发到 `readString(AcString&)`;`CMFCDbDwgFiler` 该函数直接 `return eInvalidInput`,且 `readAddress`/`writeAddress`/`seek` 全空实现。R26 下字符串读取必然失败 |
| **在 `AcDbX3dSolid` 内持有 `AcDbRegion` 对象,循环调 `dwgInFields`/`dwgOutFields`** | ① `std::vector<AcDbRegion>` 无法实例化:`AcDbObject(const AcDbObject&) = delete`(`dbobject.h:600`);② `dwgOutFields` 不写对象头,但 `AcDbEntity::dwgOutFields` 仍写 `layerId` 等 id → 未入库对象写出悬空引用;③ `Clone()` 必须深拷贝 region,而 region 不能拷贝 → 只能再序列化一遍,绘制路径成本翻倍 |
| **隐藏 BlockTableRecord / 扩展字典存 region 实体** | `AcDbX3dSolid` 非数据库对象,`Clone()` 是内存拷贝,`AcDbObjectId` 会被共享;改动面扩散到全部持有者 |
| **SAT 文本 + zlib 压缩** | 工程只有 `inc/zlib.h` 头,`CoreLibrary/unzip.cpp` 是 inflate 半边,无 deflate 实现,`dep_lib` 无 zlib 库;收益低于前三项 |
**结论:采用 SAT 的 ASCII 字节流存进 `m_pSatBuffer`。**
不改对象模型、不碰 filer、不加新成员、`Clone()` 仍是 `memcpy`。
---
## 3. 目标存储格式
### 3.1 为什么可以复用 `m_pSatBuffer`
`m_pSatBuffer` 的读写通道**已经打通并持久化**:
```cpp
// dwgOutFields :4822-4829
if (AcDbX3dSolid::kCurrentVersionNumber > 1) {
pFiler->writeUInt32(m_nSatLen);
if (m_pSatBuffer != NULL)
pFiler->writeBytes(m_pSatBuffer, m_nSatLen);
}
```
`dwgInFields` 有对称的 `readUInt32` + `readBytes`。**序列化层零改动**,只需把内容从"SAT 文本的 UTF-16 拷贝"换成"SAT 的 ASCII 字节"。
### 3.2 多 region 容器
9 种 region 类型里,5 种单 region,3 种双 region,1 种(`kLoft`)N 个 region。统一成一个容器:
```cpp
// 16 字节头
struct RegionBlobHeader {
Adesk::UInt32 magic; // 'SRGN' = 0x4E475253
Adesk::UInt16 version; // 1
Adesk::UInt16 reserved; // 0
Adesk::UInt32 nRegions; // region 个数
Adesk::UInt32 totalLen; // 总字节数(含头),用于跳过
};
// 后接 nRegions 个:
// Adesk::UInt32 len;
// char data[len]; // SAT 的 ASCII 字节,不含结尾 0
```
对应关系:
| 类型 | nRegions |
| --- | --- |
| `kExtrude2`(27) / `kSlicePipe`(26) / `kRevolve`(21) / `kRevolve2`(38) / `kSweep`(40) | 1 |
| `kConePipe`(30) / `kIntersect`(28) / `kSliceSolid`(29) | 2(外 + 内) |
| `kLoft`(41) | N |
`magic` 是**必需**的,不是可选项:`m_pSatBuffer` 是公共字节池,当前已有 4 种语义(`kFenceExtrude` 点数组、`kFrom3JS` SubDMesh、`kLoftReducer` 宽字符串、SAT 导入原始字节),必须自校验。
### 3.3 编解码
```cpp
// AcDbX3dSolid.cpp 匿名 namespace
bool PackRegions(const AcArray<AcDbRegion*>& regs, void*& pBuf, Adesk::UInt32& nLen);
bool UnpackRegions(const void* pBuf, Adesk::UInt32 nLen, AcArray<AcDbRegion*>& regs);
```
单 region 的 SAT 字节**不需要新函数**,直接复用已有的两个重载:
```cpp
// 输出 SAT 的 ASCII 字节,返回长度,pszTxt 由调用方 delete[]
Adesk::UInt64 CoreTools::RegionToString(const AcDbRegion* pReg, char*& pszTxt); // CoreTools.cpp:3114
// 从字节重建 region
AcDbRegion* CoreTools::StringToRegion(Adesk::UInt64 nLen, char* pszReg); // CoreTools.cpp:3581
```
`RegionToString(char*&)` 内部已经用 `MakeTempSatPath()` + `DeleteFile`,`StringToRegion` 内部已经过 `fixACISFormat`,阶段一、二落地后这两条链路都是现成的。`PackRegions` 负责把拿到的字节拷进容器并 `delete[]`。
---
## 4. 改动清单
> 阶段一、二已合并;阶段三暂缓,取舍与前提见第 11 节。以下保留完整方案,供重启时参考。
### 阶段一:临时文件唯一化(纯 bug 修复)
**位置**:`CoreCommand/CoreTools.cpp`
| 函数 | 现状 | 改法 |
| --- | --- | --- |
| `RegionToString` | `Region1.sat` | 唯一名 + 用完 `DeleteFile` |
| `StringToRegion(const CString&)` `:3477` | `Region2.sat` | 同上 |
| `StringToRegion(nLen, char*)` `:3525` | `Region212.sat` | 同上 |
唯一名用 `GetTempFileName` 或 `%TEMP%\{GUID}.sat`。
**同时修**:
```cpp
char* pszTxt = new char[dwFileLength + 1]; // 补 +1
::memset(pszTxt, 0, dwFileLength + 1);
UINT nRead = file.Read(pszTxt, (UINT)dwFileLength); // 检查短读
if (nRead != (UINT)dwFileLength) { delete[] pszTxt; return false; }
```
**风险**:低。**收益**:消除多实例并发时的几何错乱与越界读。
---
### 阶段二:懒重建
**位置**:`CoreLibrary/AcDbX3dSolid.cpp` / `inc/AcDbX3dSolid.h`
| 位置 | 现在 | 改成 |
| --- | --- | --- |
| `dwgInFields` 末尾 `:4753` | `ReCreate();` | `m_bNeedCreate = true;` |
| 拷贝构造末尾 `:295` | `ReCreate();` | `m_bNeedCreate = true;` |
| `inc/AcDbX3dSolid.h:255` | `AcDb3dSolid* m_pSolid;` | 加 `mutable` |
| `GetDraw3dSolid() const` `:3852` | — | 入口调 `EnsureCreated()` |
```cpp
mutable bool m_bNeedCreate; // 不序列化,默认 true
void EnsureCreated() { if (m_bNeedCreate) { m_bNeedCreate = false; ReCreate(); } }
```
**必须一起改的入口**(凡是读 `m_pSolid` 或依赖已建几何的):
- `GetInterface()`(`:3542` 附近)
- `TransformBy`(`:5496` 起)
- `GetDrawSubDMesh()`(`:3893`)
- `ToJson`
- `GetDraw2d()`(`:3933`)
**风险**:中。漏一个入口会出现"几何没建出来"。**收益**:开图不再逐单元走临时文件;`Clone()` 里的 `ReCreate()` 也一并省掉。
---
### 阶段三:region 数据二进制化
#### 4.3.1 写入点(`RegionToString` → `PackRegions`)
行号为当前 main(阶段一、二落地后),实施时以函数名定位。
| 行 | 类型 / 函数 | 现状 |
| --- | --- | --- |
| `:1371` | `kExtrude2` | `RegionToString(pReg, m_sScr)` |
| `:1436`、`:1439` | `kConePipe` | 外/内 → `sScr1`/`sScr2` → 拼 `m_sScr` |
| `:2041` | `kSlicePipe` | → `m_sScr` |
| `:2127` | `kIntersect` | → `sScr1` → `m_sScr` |
| `:2181` | `kSliceSolid` | → `sSrc1` → `m_sScr` |
| `:2211` | `kRevolve` | → `m_sScr` |
| `:2242` | `kRevolve2` | → `m_sScr` |
| `:5029` | `kLoft` | `strAll`(`@region@` 拼接)→ `m_sScr` |
| `:5087`、`:5101` | `kSweep` | → `m_sScr` |
| `:4882`、`:4895` | `SetSolidData` | `m_sScr = srcRegion` |
| `:5135`、`:5155`、`:5176`、`:5206` | `SetExturd2Data` / `SetRevolve2Data` / `SetSweepData` / `SetLoftData` | `m_sScr = sReg` |
| `:5516`、`:5528`、`:5542`、`:5562`、`:5589` | `TransformBy` | 变换后重新导出 |
#### 4.3.2 读取点(`StringToRegion(m_sScr)` → `UnpackRegions`)
| 行 | 类型 / 函数 |
| --- | --- |
| `:4366` | `kSlicePipe` |
| `:4378` | `kExtrude2` |
| `:4402` | `kIntersect` |
| `:4440` | `kSliceSolid` |
| `:4468` | `kRevolve` |
| `:4488`、`:4489` | `kConePipe` |
| `:4563` | `kRevolve2` |
| `:4587` | `kLoft`(循环) |
| `:4601` | `kSweep` |
| `:5321`、`:5345`、`:5370`、`:5453` | `ToJson` |
| `:5512`、`:5524`、`:5538`、`:5558`、`:5584` | `TransformBy` |
#### 4.3.3 序列化
**不改**。`m_pSatBuffer` 的 `writeUInt32` + `writeBytes` 已经在 `:4822-4829`,`dwgInFields` 有对称读。
只改一件事:region 类型写入时**不再写 `m_sScr`**(保持空字符串),管廊脚本类型不受影响。
#### 4.3.4 兼容判断
```cpp
bool HasNewRegionData() const {
if (!m_pSatBuffer || m_nSatLen < sizeof(RegionBlobHeader)) return false;
return ((const RegionBlobHeader*)m_pSatBuffer)->magic == kRegionBlobMagic;
}
```
读取:
```cpp
if (HasNewRegionData())
UnpackRegions(m_pSatBuffer, m_nSatLen, regs); // 新格式
else if (!m_sScr.IsEmpty())
ParseLegacyRegions(m_sScr, regs); // 老图纸,沿用现有切分逻辑
```
写入:只写新格式。
**不升 `kCurrentVersionNumber`**。理由:字段都在,靠"哪个非空"即可判别;升版本会让老版本 CAD 读新图时走 `eMakeMeProxy`,新图在所有老版本上彻底不可编辑。代价是老版本看不到 region 类型的几何,这是已定的取舍,见 11.1。
#### 4.3.5 `kLoftReducer` 纳入统一容器
`CreateLoftReducer`(`:3015`)当前已把多 region 串写进 `m_pSatBuffer`(`:3118-3121`),格式是 `TCHAR` 宽字符 + `TCHAR(0x00c1)` 分隔 —— 属于半迁移状态,体积没减半。
本次一并纳入统一容器:
- 写入:`PackRegions` 替代 `:3118-3121` 的 TCHAR 拷贝
- 读取:`GetLoftReducerSides`(`:4309` 附近)先验 `magic`;命中走 `UnpackRegions`,未命中回退到现有的 `0x00c1` 切分(老图纸)
- `kLoftReducer` 的 `m_sszPoint2ds` 存"下口|上口"点串(`:3090-3092`),与 region 容器互不影响,保持不变
#### 4.3.6 `m_sScr` 的去留
`kEarthWorm`(7) 与 `kSHEarthWorm`(23) **均已废弃**,不再产生新数据,但存量图纸仍需可读。
**本次保留 `m_sScr` 字段不删**,仅停止写入:
- 写入:region 类型不再写 `m_sScr`(保持空字符串)
- 读取:`kEarthWorm` / `kSHEarthWorm` 的脚本分支保留,保证老图纸可读
- 字段本身、`dwgOutFields` / `dwgInFields` 的 `writeString` / `readString` 调用全部保留
后续等存量图纸淘汰后,再单独清理字段和读写调用。
---
## 5. 公开接口处理
以下接口签名是 `CString`,外部有调用方(`AecDbBDPipeBase.cpp` 6 处、`Pool3dCmd/DTYGDlg.cpp` 1 处、`Pool3dCmd/DTYG_MODIFY.cpp` 8 处):
```cpp
bool GetExturd2Data(CString& sReg, AcGePoint3d& ptFr, AcGePoint3d& ptTo); // :5106
void SetExturd2Data(const CString& sReg, const AcGePoint3d&, const AcGePoint3d&); // :5115
bool GetRevolve2Data(CString& sReg, AcGePoint3d&, AcGeVector3d&, double&, double&); // :5126
void SetRevolve2Data(const CString& sReg, ...); // :5137
bool GetSweepData(CString& sReg, AcDbCurve*& pCurve); // :5150
void SetSweepData(const CString& sReg, const AcDbCurve* pCurve); // :5158
```
**保持签名不变**。内部实现改为:
- `SetXxxData`:把传入的文本按老分隔符解析成 region 数组 → `PackRegions`
- `GetXxxData`:`UnpackRegions` → 拼回原有文本格式返回
`CString` 无法可靠承载二进制(含 `\0` 会截断),所以对外仍是"文本 SAT"语义,转换只发生在这一层,调用方无感。这些是对话框级低频调用,开销可接受。
`SetSatBuffer` / `GetSatBuffer`(`:4971` / `:4980`)是公开裸接口,外部 `Pool3dCmd/DTYGDlg.cpp:805/4371` 在用。它们操作的是 SubDMesh 类数据,与 region 不冲突,靠 `magic` 隔离即可。
---
## 6. 风险清单
| # | 风险 | 影响 | 应对 |
| --- | --- | --- | --- |
| 1 | `m_pSatBuffer` 语义冲突 | 把点数组当 region 容器解析,崩溃 | 强制 `magic` 校验,不匹配一律走老路径 |
| 2 | 漏改某个 `ReCreate` 入口 | 几何不显示 | 阶段二逐入口核对,用 `EnsureCreated()` 统一收口 |
| 3 | 老图纸读取回归 | 存量图纸打不开 | 保留 `ParseLegacyRegions`,不做自动迁移 |
| 4 | `kLoft` 的 `@region@` 分隔 | 多 region 顺序错乱 | 容器改成"数量 + 长度前缀",顺序显式 |
| 5 | 跨模块链接 | `CoreTools::RegionToBuffer` 在 CoreCommand,CoreLibrary 是静态库 | 已有 `CoreTools::RegionToString` 先例,照现有模式 |
| 6 | 新旧格式并存 | 代码里长期保留两条读路径 | 可接受;等存量图纸淘汰后再清理 |
---
## 7. 验证清单
编译:仅 `R190` + `Debug`,目标 `AecRxBase`、`CoreLibrary_BAL_REMOTE`/`_TRIAL_YEAR`、`CoreCommand_BAL_REMOTE`/`_TRIAL_YEAR`、`CoreImplement`。
功能(必须在 CAD 内实测):
1. **新建 + 存盘 + 重开**:9 个 region 类型的几何与改前一致(对比 `getArea`、体积、`getGeomExtents`)
2. **老图纸兼容**:打开改动前保存的图,走 `m_sScr` 路径,几何正确
3. **移动 / 旋转**:`kExtrude2`、`kRevolve`、`kRevolve2`、`kLoft`、`kSweep` 变换后几何跟着动(`TransformBy` 的 5 处往返)
4. **复制粘贴 / 撤销**:验证 `Clone()` 路径(`AecDbXDraw::add3DCache`)不崩、不丢几何
5. **对话框往返**:`DTYGDlg`、`DTYG_MODIFY` 的 `GetXxxData`/`SetXxxData` 行为不变
6. **并发**:同时开两个 CAD 实例打开含 region 的图,几何无错乱(验证阶段一)
指标:
- 开图耗时(阶段二)
- 存盘后 DWG 体积(阶段三,预期 region 部分减半)
- `Clone()` 耗时(阶段二后应显著下降)
---
## 8. 明确不做
| 项 | 原因 |
| --- | --- |
| 存构建曲线 + `createFromCurves` | 拿不到原始曲线,布尔结果不可逆 |
| 自写 `AcDbDwgFiler` | R26 `readString(ACHAR**)` 已 `final` |
| `AcDbX3dSolid` 持有 `AcDbRegion` 对象 | 拷贝构造 `= delete`,`Clone()` 退化 |
| 隐藏 BTR / 扩展字典 | 对象模型改动过大,`AcDbObjectId` 共享问题 |
| zlib 压缩 | 工程无 deflate 实现,收益低于前三项 |
| 修 `CMFCDbDwgFiler` 的 R26 字符串问题 | 该类已不再使用 |
| 补 `ToJson` 的 `kEarthWorm` 分支 | `kEarthWorm` 已废弃 |
| 删除 `m_sScr` 字段 | `kEarthWorm` / `kSHEarthWorm` 已废弃,但存量图纸仍需可读;本次保留字段,后续单独清理 |
| 自动迁移老图纸 | 任何一次保存都会改写图纸数据,风险过高 |
---
## 9. 实施顺序
| 顺序 | 阶段 | 改动量 | 风险 | 收益 |
| --- | --- | --- | --- | --- |
| 1 | 一:临时文件唯一化 | 小 | 低 | 消除并发错乱、越界读 |
| 2 | 二:懒重建 | 中 | 中 | 开图性能(最大) |
| 3 | 三:region 二进制化 | 大 | 中高 | DWG 体积减半、去编码转换 |
每个阶段独立提交、独立 PR,前一阶段验证通过后再进下一阶段。
---
## 10. 已确认事项
| # | 事项 | 结论 |
| --- | --- | --- |
| 1 | `kLoftReducer`(22) 是否纳入统一容器 | **纳入**(4.3.5),老数据回退 `0x00c1` 切分 |
| 2 | `kSHEarthWorm`(23) 是否仍在使用 | **已废弃**,但 `m_sScr` 字段本次保留不删,仅停止写入(4.3.6) |
| 3 | 公开 `CString` 接口 | **签名不变**,内部转换,外部调用方零改动(第 5 节) |
---
## 11. 阶段三暂缓(2026-10-22)
**决定:暂不实施。** 阶段一(临时文件唯一化、越界读)、阶段二(懒重建)已合并,主要痛点已经解决;阶段三剩下的收益只有「region 部分 DWG 体积减半」,代价是新图在老版本插件里 region 几何不可见。当前时机未到。
### 11.1 兼容方向(已定)
**单向兼容**:新版插件完整读取旧版图纸(`m_sScr` 老路径永久保留);旧版插件打开新图时,region 类型的 `AcDbX3dSolid` 几何不可见。
不可见不等于丢数据:`m_pSatBuffer` 老版本也会原样读写(版本 > 1 的通道一直都在),所以旧版打开后另存、再回新版打开,几何仍能恢复。非 region 类型(`m_sszPoint2ds` 点串、SubDMesh、管廊脚本)不受影响。
| 选项 | 后果 | 结论 |
| --- | --- | --- |
| 不升版本号,接受老插件缺几何 | 老插件能开图、参数都在,region 类型几何不显示;换新版可恢复 | **已定,选这个** |
| 升到 5,老插件显式 proxy | 不会静默缺几何,但新图在所有老版本上彻底不可编辑,且不可回退 | 不选 |
| 保留双写(`m_sScr` 仍写老格式) | 体积变成 2N + N,比现状更差 | 否决 |
### 11.2 重启前还要定的事
1. `TransformBy`、`ToJson` 是否一起改——这两处在运行时会反复导出/解析 SAT,是阶段三改动量的大头
2. 老图纸里 region 类型的实际占比——占比低的话体积收益有限,可以永久搁置
### 11.3 实施时的技术前提(已核对)
- `kCurrentVersionNumber` 当前是 **4**。`m_pSatBuffer` 的读写通道在 `dwgOutFields:4822` / `dwgInFields:4754`,版本 > 1 即生效,阶段三不需要动序列化
- **不需要**新增 `CoreTools::RegionToBuffer`:`RegionToString(const AcDbRegion*, char*&)`(`CoreTools.cpp:3114`)已经输出 SAT 的 ASCII 字节和长度,内部用 `MakeTempSatPath` + `DeleteFile`
- 读端 `StringToRegion(Adesk::UInt64, char*)`(`CoreTools.cpp:3581`)已经过 `fixACISFormat`
- `m_pSatBuffer` 当前有 4 种语义:`kFenceExtrude` 点数组(`:1514`)、`kFrom3JS` SubDMesh(`:1019`/`:1048`)、`kLoftReducer` 宽字符串(`:3132`/`:5405`)、SAT 导入原始字节(`:2872`)。容器头的 magic 是必需的
- 拷贝构造对 `m_pSatBuffer` 做了深拷贝(`:274-280`),`Clone()` 不会共享指针
### 11.4 当前状态
| 阶段 | 状态 | PR |
| --- | --- | --- |
| 一:临时文件唯一化 | 已合并 | #22、#26 |
| 二:懒重建 | 已合并 | #25、#27 |
| 三:region 二进制化 | 暂缓 | — |