Files
envi-code/AcDbX3dSolid-refactor-plan.md
T
company-desktop-computer e65eba26c8 docs: 新增 AcDbX3dSolid 重构计划
- 现状:m_sScr 用 CString 存 ACIS SAT,UTF-16 导致体积翻倍;临时文件名写死;开图逐单元走临时文件
- 否决方案:存构建曲线、自写 AcDbDwgFiler、持有 AcDbRegion 对象、隐藏 BTR、zlib 压缩
- 采用方案:SAT 的 ASCII 字节流存进 m_pSatBuffer(RegionBlobHeader + 长度前缀)
- 三阶段:临时文件唯一化 -> 懒重建 -> region 数据二进制化
- 兼容策略:读兼容老图纸(m_sScr / 0x00c1 切分),写入统一新格式,不自动迁移
- 公开 CString 接口签名不变,内部转换
- kLoftReducer 纳入统一容器;kEarthWorm/kSHEarthWorm 已废弃但 m_sScr 字段暂不删除
2026-10-20 16:20:48 +08:00

357 lines
16 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`。
>
> 原则:**老图纸必须能读**;写入统一新格式;不改对象模型(`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 :4808-4814
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 内部 static
static bool PackRegions(const AcArray<AcDbRegion*>& regs, void*& pBuf, Adesk::UInt32& nLen);
static bool UnpackRegions(const void* pBuf, Adesk::UInt32 nLen, AcArray<AcDbRegion*>& regs);
// 单 region 的 SAT 字节(CoreTools 新增,与 RegionToString 共用临时文件逻辑)
bool CoreTools::RegionToBuffer(const AcDbRegion* pReg, void*& pBuf, Adesk::UInt32& nLen);
```
`UnpackRegions` 内部用**已存在**的 `CoreTools::StringToRegion(Adesk::UInt64 nLen, char* pszReg)`(`CoreTools.cpp:3525`),读端无需新代码。
---
## 4. 改动清单
### 阶段一:临时文件唯一化(纯 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`)
| 行 | 类型 | 现状 |
| --- | --- | --- |
| `:1364` | `kExtrude2` | `RegionToString(pReg, m_sScr)` |
| `:1429-1432` | `kConePipe` | 外/内两个 → `sScr1`/`sScr2` → 拼 `m_sScr` |
| `:2034` | `kSlicePipe` | `RegionToString(pRegion, m_sScr)` |
| `:2120` | `kIntersect` | → `sScr1` → `m_sScr` |
| `:2174` | `kSliceSolid` | → `sSrc1` → `m_sScr` |
| `:2204` | `kRevolve` | → `m_sScr` |
| `:2235` | `kRevolve2` | → `m_sScr` |
| `:5020` | `kLoft` | `strAll`(`@region@` 拼接)→ `m_sScr` |
| `:5073`、`:5087` | `kSweep` | → `m_sScr` |
| `:4868`、`:4881` | `SetSolidData` | `m_sScr = srcRegion` |
| `:5121`、`:5141`、`:5162`、`:5192` | `SetExturd2Data` / `SetRevolve2Data` / `SetSweepData` / `SetLoftData`(`:5192` 所属函数实施时核对) | `m_sScr = sReg` |
#### 4.3.2 读取点(`StringToRegion(m_sScr)` → `UnpackRegions`)
| 行 | 类型 |
| --- | --- |
| `:4364` | `kSlicePipe` |
| `:4376` | `kExtrude2` |
| `:4400` | `kIntersect` |
| `:4438` | `kSliceSolid` |
| `:4466` | `kRevolve` |
| `:4486-4487` | `kConePipe` |
| `:4561` | `kRevolve2` |
| `:4585` | `kLoft` |
| `:4599` | `kSweep` |
| `:5307`、`:5331`、`:5356`、`:5439` | `ToJson` |
| `:5500`、`:5512`、`:5526`、`:5542`、`:5571` | `TransformBy` |
| `:5111`、`:5129`、`:5154` | `GetExturd2Data` / `GetRevolve2Data` / `GetSweepData` |
#### 4.3.3 序列化
**不改**。`m_pSatBuffer` 的 `writeUInt32` + `writeBytes` 已经在 `:4808-4814`,`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`。
#### 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 节) |