Files
envi-code/AcDbX3dSolid-refactor-plan.md
T
company-desktop-computer cad5289fae docs: 补阶段三的降级导出方案(新格式 → 老格式)
新增第 12 节。核心结论:降级不需要重建几何,容器里就是 acisOut 的 SAT
ASCII 字节,老 m_sScr 里也是同一批字节,解包后按老规则拼回即可,纯内存、
不调 acisIn、不落临时文件。

- 在 dwgOutFields 里做:AcDbX3dSolid 的持有关系分散在 X3dPart、X3dPartEntity
  以及 Hanger/Roof/Stair 的 m_solids 缓存里,命令层遍历不全,但存盘时每个
  实例必然走 dwgOutFields
- 进程级开关 + RAII 复位,只改写出内容,不改对象状态,导出后当前图仍是新格式
- MakeLegacyScr 按 3.2 的表分四类拼回,容器写出长度 0
- 12.6 指出 4.3.6「region 类型停写 m_sScr」与 kIntersect/kSliceSolid 的折线
  后缀冲突,建议这两种类型继续只写折线后缀
- 12.8 记录限制:导出写的对象版本号是 4,只能被 kCurrentVersionNumber >= 4
  的插件读取;版本号 3 的存量插件靠降级救不回来,需要另写 v3 布局

11.1、11.2、11.4 同步加交叉引用与状态行。
2026-10-22 13:26:51 +08:00

544 lines
27 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 种类型里,6 种只有 region(5 种单 region、1 种双 region),2 种是 region + 折线,1 种(`kLoft`)N 个 region。容器只装 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
```
对应关系(按当前代码核对):
| 类型 | `m_sScr` 实际内容 | nRegions |
| --- | --- | --- |
| `kExtrude2`(27) / `kSlicePipe`(26) / `kRevolve`(21) / `kRevolve2`(38) / `kSweep`(40) | 单个 region 的 SAT | 1 |
| `kConePipe`(30) | `外环SAT` + `\|` + `内环SAT` | 2 |
| `kIntersect`(28) / `kSliceSolid`(29) | `regionSAT` + `\|` + **折线文本** | 1 |
| `kLoft`(41) | N 个 region SAT,`@region@` 连接 | N |
几点必须注意:
- `kIntersect` / `kSliceSolid` 的第二个分量是 `CoreTools::CurveToString` 产出的折线文本,**不是 region**。写端 `:2130`/`:2184`,读端用 `StringToPolyline`(`:4403`、`:4441`)。原文档把它们当成「外 + 内」两个 region 是错的,按那样装容器,解包时第二个 `acisIn` 必然失败
- 切分统一取第一个 `|`,且 `sScr2 = m_sScr.Mid(nPos)` 保留了分隔符本身,靠 `StringToRegion` / `StringToPolyline` 内部找 ACIS 头容错
- `kSweep` 的路径存在 `m_sszPoint2ds`(`AcDbCurveToString`,`:5088`/`:5103`),不在 `m_sScr`
- 将来做降级导出时也要按这张表拼回,`kIntersect` / `kSliceSolid` 的折线文本要原样保留
`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、管廊脚本)不受影响。
需要把图交给老版本用户时,用第 12 节的降级导出:存盘瞬间把容器翻成老格式的 `m_sScr`,老插件就能正常显示几何,当前文档本身不受影响。
| 选项 | 后果 | 结论 |
| --- | --- | --- |
| 不升版本号,接受老插件缺几何 | 老插件能开图、参数都在,region 类型几何不显示;换新版可恢复 | **已定,选这个** |
| 升到 5,老插件显式 proxy | 不会静默缺几何,但新图在所有老版本上彻底不可编辑,且不可回退 | 不选 |
| 保留双写(`m_sScr` 仍写老格式) | 体积变成 2N + N,比现状更差 | 否决 |
### 11.2 重启前还要定的事
1. `TransformBy`、`ToJson` 是否一起改——这两处在运行时会反复导出/解析 SAT,是阶段三改动量的大头
2. 老图纸里 region 类型的实际占比——占比低的话体积收益有限,可以永久搁置
3. `kIntersect` / `kSliceSolid` 的折线后缀怎么存(12.6)——阶段三动 `m_sScr` 之前必须先定,否则折线文本会丢
### 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 二进制化 | 暂缓 | — |
| 降级导出(阶段三配套) | 方案已定,未实施 | — |
---
## 12. 降级导出(新格式 → 老格式)
### 12.1 结论
可行,而且不需要重建几何。容器里存的就是 `acisOut` 出来的 SAT ASCII 字节,老 `m_sScr` 里存的也是同一批字节;降级只是「解包容器 → 按老规则拼字符串」,纯内存操作,不调 `acisIn`、不落临时文件、不损失任何东西。
这让阶段三的兼容代价从「老版本永远看不到新图几何」变成「平时用新格式,交付给老版本用户前导一次」。
### 12.2 为什么在 `dwgOutFields` 里做
`AcDbX3dSolid` 的持有关系是分散的:`AecDbX3dPart`、`AecDbX3dPartEntity`,以及 `AecDbHanger` / `AecDbRoof` / `AecDbStair` 各自的 `m_solids` 缓存。从命令层遍历找不全所有实例,但存盘时每个实例一定会走 `dwgOutFields`。
所以在序列化层加一个进程级开关:开关打开时,`dwgOutFields` 把 region 容器翻译成老格式的 `m_sScr`,并抑制容器写出。
### 12.3 接口
```cpp
// AcDbX3dSolid.h
static void SetLegacyOutput(bool b);
static bool IsLegacyOutput();
private:
bool MakeLegacyScr(CString& sScr) const; // 解包容器,按 m_eType 拼回老格式
bool HasNewRegionData() const; // 见 4.3.4
```
`MakeLegacyScr` 成功返回 true;容器损坏、解包不出 region 时返回 false,调用方退回写新格式(宁可不降级,也不写出空几何)。
### 12.4 `dwgOutFields` 的改动
只有两处:`:4797` 写 `m_sScr`、`:4822-4829` 写容器。
```cpp
CString sScrOut = m_sScr;
const void* pSatOut = m_pSatBuffer;
Adesk::UInt32 nSatOut = m_nSatLen;
if (IsLegacyOutput() && HasNewRegionData() && MakeLegacyScr(sScrOut))
{
pSatOut = NULL; // 容器不写出,老插件读到长度 0
nSatOut = 0;
}
pFiler->writeString((LPCTSTR)sScrOut);
...
pFiler->writeUInt32(nSatOut);
if (pSatOut != NULL)
pFiler->writeBytes(pSatOut, nSatOut);
```
关键点:
- **不改对象状态**。`m_sScr` / `m_pSatBuffer` 都不动,只换写出的内容,导出后当前图纸仍是新格式,可以继续编辑、继续存盘
- 容器写 0 长度,老插件 `readUInt32` 读到 0 直接跳过,不会报错
- 对象版本号仍写 4,不升
### 12.5 `MakeLegacyScr` 的拼法(按 3.2 的表)
| 类型 | 容器里 | 拼回 `m_sScr` |
| --- | --- | --- |
| `kExtrude2` / `kSlicePipe` / `kRevolve` / `kRevolve2` / `kSweep` | 1 个 region | region 的 SAT 文本 |
| `kConePipe` | 2 个 region(外、内) | `外环SAT` + `\|` + `内环SAT` |
| `kIntersect` / `kSliceSolid` | 1 个 region | `regionSAT` + `m_sScr`(折线后缀,见 12.6) |
| `kLoft` | N 个 region | N 段 SAT 用 `@region@` 连接 |
读端能容忍 `|` 留在第二段:`kConePipe` 用 `StringToRegion(sScr2)`(`:4489`)、`kIntersect` / `kSliceSolid` 用 `StringToPolyline(sScr2)`(`:4403`、`:4441`),两者内部都靠找 ACIS 头 / 数值容错。
### 12.6 阶段三必须先把折线后缀的位置定下来
`kIntersect` / `kSliceSolid` 的老格式是 `regionSAT|折线文本`,折线文本不是 region,不进容器。而 4.3.6 要求 region 类型停写 `m_sScr`——这两条冲突。
**建议**:这两种类型继续写 `m_sScr`,但只写老格式的第二段(含开头的 `|`,即原来的 `sScr2`)。理由:
- 体积上只多了折线文本,region 那部分照常减半
- 读端 `sScr2 = m_sScr.Mid(nPos)` 的既有切分逻辑不用改
- 降级导出直接 `regionSAT + m_sScr`,不需要额外字段
不这么做的话,折线只能塞进容器做第二个 section,容器格式要加类型标签,复杂度和风险都上一个台阶。
### 12.7 命令
```cpp
void SaveLegacyDwg()
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
// 目标文件:当前图名 + _legacy.dwg,或让用户选
ScopedLegacyOutput guard; // 构造时置位,析构复位
Acad::ErrorStatus es = pDb->saveAs(szTarget, false, AcDb::kDHL_CURRENT);
if (es != Acad::eOk)
acutPrintf(_T("\n导出失败,err=%d"), es);
}
```
- `saveAs(file, false, ...)` 的第二个参数是 `bBakAndRename`,传 `false` 只写副本、不切换当前文档(`dbmain.h:473`)
- 开关用 RAII 复位,命令异常、用户取消都不会留下「后续存盘全变老格式」的状态
- 只处理当前数据库,外部参照里的 `AcDbX3dSolid` 不在范围内
### 12.8 一个必须说清的限制:只能兼容 `kCurrentVersionNumber >= 4` 的插件
`dwgOutFields` 写出的对象版本号固定是 4(`:4776`)。老插件的 `dwgInFields` 一旦发现 `version > kCurrentVersionNumber` 就返回 `eMakeMeProxy`(`:4682`),整个对象变代理,降级内容一点都读不到。
`dwgInFields` 里还留着 `version > 2 && version < 4` 的分支(`:4759`),说明线上确实存在版本号 3 的插件。**这些插件靠降级导出救不回来**——要兼容它们得额外写 v3 布局(`kSweep` 的路径要以 polyline 内联在对象字段之后),工作量另算,本次不做。
降级导出的目标客户是「版本号已经是 4、只是不认识容器」的插件。
### 12.9 验证清单
- 9 种类型各建一个样本,导出后装到老版本插件里打开,几何、面积、位置都正确
- 老版本打开导出的图 → 另存 → 回新版打开,几何仍正确(回环)
- 导出前后,当前文档自身存盘仍是新格式(开关已复位)
- 容器被人为改坏(magic 对、region 段截断)时,`MakeLegacyScr` 返回 false,导出退回新格式并打日志,不产出空几何
- `kIntersect` / `kSliceSolid` 的折线后缀原样保留,读回后截面边界正确
### 12.10 成本与时机
| 项 | 量 |
| --- | --- |
| 开关 + `dwgOutFields` 分支 | ~30 行 |
| `MakeLegacyScr` 按类型拼回 | ~100 行 |
| 导出命令 + 文件选择 | ~40 行 |
| 主要风险 | 老格式规则核对不全,导出的图在老版本里读不出来 |
依赖 12.6 的折线后缀方案先定。与阶段三的先后关系:两者最好一起上——阶段三一旦合入、`m_sScr` 停写,没有降级导出的那段时间里,新图在老版本插件上就是缺几何的。