Files
envi-code/AcDbX3dSolid-refactor-plan.md
T
company-desktop-computer f0bf3f8fe2 docs: 补开图速度优化方案
第 13 节从「两个候选方向」扩写成可执行方案:

- 13.1 成本模型:dwgInFields:1824 无条件 Create();Create() 每次全量重建,
  AddX3dSolid 不置脏,调用方只能靠再调 Create() 保证同步
- 13.2 方案 A(推荐,不改格式),分两步:
  A1 加 m_bDirty,Create() 早退,改参数的 15 个接口置脏
  A2 dwgInFields 去掉 Create(),5 个读入口统一走 EnsureCreated
  附收益边界与四个风险点(副作用延迟、失败重试、const_cast、CopyFrom 冗余)
- 13.3 方案 B 缓存布尔结果:dwg 字段要升版本(不接受)、扩展字典 xrecord
  可兼容但体积膨胀,结论是量完 A 再定
- 13.4 方案 C 降低单次重建成本:bbox 剔除、减少 clone、DrawUserInf3d 热点
- 13.5 度量埋点与验收判据
- 13.6 建议顺序:A1 → A2 → 再决定 B/C
2026-10-22 14:46:18 +08:00

644 lines
34 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`。
>
> 状态(2026-10-22):阶段一、二已合并;**阶段三(region 二进制化)决定不做**,存储格式保持现状,新老插件双向可读。理由见第 11 节,后续方向见第 13 节。
---
## 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. 目标存储格式
> 阶段三已取消(第 11 节),以下为存档方案。
### 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 类型里,**只有 7 种参与迁移**:5 种单 region、1 种双 region(`kConePipe`)、1 种 N region(`kLoft`)。`kIntersect`(28) / `kSliceSolid`(29) 是遗留类型,全库没有产生路径,不参与迁移(见下)。容器只装 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 | 是 |
| `kLoft`(41) | N 个 region SAT,`@region@` 连接 | N | 是 |
| `kIntersect`(28) / `kSliceSolid`(29) | `regionSAT` + `\|` + 折线文本 | — | **否**,遗留类型,`m_sScr` 保持原样 |
几点必须注意:
- `kIntersect` / `kSliceSolid` 不参与迁移:不进容器、不停止写 `m_sScr`,读写路径保持原样。它们的老格式是 `regionSAT|折线文本`,第二个分量是 `CoreTools::CurveToString` 产出的折线(`:2130`/`:2184`),**不是 region**;读端用 `StringToPolyline`(`:4403`、`:4441`)。原文档把它们当成「外 + 内」两个 region 是错的
- 切分统一取第一个 `|`,且 `sScr2 = m_sScr.Mid(nPos)` 保留了分隔符本身,靠 `StringToRegion` / `StringToPolyline` 内部找 ACIS 头容错
- `kSweep` 的路径存在 `m_sszPoint2ds`(`AcDbCurveToString`,`:5088`/`:5103`),不在 `m_sScr`
- 降级导出按这张表拼回:迁移的 7 种走容器,遗留的 2 种原样保留 `m_sScr`
`magic` 是**必需**的,不是可选项:`m_pSatBuffer` 是公共字节池,当前已有 4 种语义(`kFenceExtrude` 点数组、`kFrom3JS` SubDMesh、`kLoftReducer` 宽字符串、SAT 导入原始字节),必须自校验。
**`kIntersect` / `kSliceSolid` 为什么排除在外**(2026-10-22 核对):
- 枚举只在 `inc/AcDbX3dSolid.h:53-54` 定义,其它模块零引用
- `CreateIntersect` / `CreateSliceSolid` 只有三个出现点:定义(`:2050`、`:2136`)、头文件声明、`ReCreate` 的读分支调用(`:4413`、`:4451`)—— 是纯读路径,只为重建图纸里已有的对象
- `Create()` 的 12 个 case 里没有它们;`SetSolidData()` 全库无调用者,内部也没有对应 case
- 没有 `(Primitive)28` / `(Primitive)29` 这类数字强转
结论:新图永远不会产生这两种对象,阶段三不必为它们设计容器格式。
### 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` |
| `: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` | 变换后重新导出 |
`kIntersect`(`:2127`) 与 `kSliceSolid`(`:2181`) 不迁移,写入保持现状(见 3.2)。
#### 4.3.2 读取点(`StringToRegion(m_sScr)` → `UnpackRegions`)
| 行 | 类型 / 函数 |
| --- | --- |
| `:4366` | `kSlicePipe` |
| `:4378` | `kExtrude2` |
| `:4468` | `kRevolve` |
| `:4488`、`:4489` | `kConePipe` |
| `:4563` | `kRevolve2` |
| `:4587` | `kLoft`(循环) |
| `:4601` | `kSweep` |
| `:5321`、`:5345`、`:5370`、`:5453` | `ToJson` |
| `:5512`、`:5524`、`:5538`、`:5558`、`:5584` | `TransformBy` |
`kIntersect`(`:4402`) 与 `kSliceSolid`(`:4440`) 的读分支保留原样,不改成 `UnpackRegions`。
#### 4.3.3 序列化
**不改**。`m_pSatBuffer` 的 `writeUInt32` + `writeBytes` 已经在 `:4822-4829`,`dwgInFields` 有对称读。
只改一件事:迁移的 7 种 region 类型写入时**不再写 `m_sScr`**(保持空字符串)。`kIntersect` / `kSliceSolid` 例外,继续按老格式写;管廊脚本类型不受影响。
#### 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 节。
#### 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. **新建 + 存盘 + 重开**:迁移的 7 个 region 类型的几何与改前一致(对比 `getArea`、体积、`getGeomExtents`);`kIntersect` / `kSliceSolid` 不迁移,只需确认老图仍能打开
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 | 二:懒重建 | 中 | 中 | 开图性能(实际最大瓶颈在 `X3dPart::Create`,见 13) |
| 3 | 三:region 二进制化 | 大 | 中高 | DWG 体积减半、去编码转换(**已取消**) |
每个阶段独立提交、独立 PR,前一阶段验证通过后再进下一阶段。
---
## 10. 已确认事项
| # | 事项 | 结论 |
| --- | --- | --- |
| 1 | `kLoftReducer`(22) 是否纳入统一容器 | 原定**纳入**(4.3.5),随阶段三取消 |
| 2 | `kSHEarthWorm`(23) 是否仍在使用 | **已废弃**,但 `m_sScr` 字段本次保留不删,仅停止写入(4.3.6) |
| 3 | 公开 `CString` 接口 | **签名不变**,内部转换,外部调用方零改动(第 5 节) |
---
## 11. 阶段三取消(2026-10-22)
**决定:不实施。** 两条理由:
1. 阶段三的主要收益是图纸体积(region 部分约减半),而当前图纸大小在可接受范围内,这个收益不值一次格式变更
2. 改动会改变存储格式:新图里的 region 类型在老插件上读不出几何,要么升 `kCurrentVersionNumber` 变成 proxy,要么额外做降级导出兜底,两头都要付代价,性价比不足
阶段一、二已经解决了并发临时文件错乱、越界读、开图时的重复重建,剩下的痛点不足以支撑一次格式变更。开图速度的问题另有更直接的着手点,见第 13 节。
### 11.1 取消后的兼容状态
存储格式保持现状:`m_sScr` 继续写 UTF-16 的 SAT 文本,`m_pSatBuffer` 的语义也不变。新老插件双向可读,不存在单向兼容问题,也不需要第 12 节的降级导出。
> 存档:当初评估过三个兼容选项——不升版本(老插件缺几何,换新版可恢复)、升到 5(老插件显式 proxy)、双写(体积更差)——都随阶段三取消而不适用。
### 11.2 重启的前提
1. 图纸体积重新成为瓶颈——阶段三的收益只有这一项
2. 能接受格式变更,或愿意同时做降级导出(第 12 节)
3. `TransformBy`、`ToJson` 与 4 组 Get/Set 必须一起改,核对结果见下
`m_sScr` 在 `AcDbX3dSolid.cpp` 共 66 处、24 个函数:`ReCreate` 18、`TransformBy` 10、`ToJson` 9、9 个 `Create*` 各 1、4 组 Get/Set 各 1、`dwgIn/OutFields` 各 1。范围是「所有 `m_sScr` 读写者」,不存在「只改生成路径」的做法:
- `TransformBy`(`:5488`)5 个类型都靠 `StringToRegion(m_sScr)` 取截面,取不到就 `bChanged = false`,末尾不 `ReCreate`。`kExtrude2` 会留下「参数已变换、几何未变」的不一致;`kRevolve` / `kRevolve2` / `kSweep` 整段分支不执行,完全不动
- `kLoft` 更严重:`vec` 为空导致 `strAll` 为空,`:5567` 把 `m_sScr` 覆盖成空字符串,`ReCreate` 里 `CreateLoft` 因空输入直接 `return false`(`:5006`)——一次移动就永久丢掉截面数据
- `ToJson`(`:5211`)三个分支(`kSweep`、单 region、多 region)全靠 `m_sScr` 解析出 `__sScr__`,不改则导出的 JSON 里 region 几何为空
- 4 组 Get/Set(`:5120`~`:5209`)直接读写 `m_sScr`,外部 15 处调用方(`AecDbBDPipeBase.cpp` 6、`DTYGDlg.cpp` 1、`DTYG_MODIFY.cpp` 8)依赖它们
### 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 二进制化 | **取消**(2026-10-22) | — |
| 降级导出(阶段三配套) | 取消,方案存档于第 12 节 | — |
---
## 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` | 不进容器 | 不涉及,`m_sScr` 原样写出 |
| `kLoft` | N 个 region | N 段 SAT 用 `@region@` 连接 |
`kConePipe` 的读端用 `StringToRegion(sScr2)`(`:4489`),`sScr2` 带着开头的 `|`,靠 `StringToRegion` 内部找 ACIS 头容错,所以拼回时照原样加 `|` 即可。
### 12.6 `kIntersect` / `kSliceSolid` 不参与迁移(已定)
这两种是遗留类型,全库没有产生路径(核对见 3.2),只可能出现在早期版本写出的图纸里。因此:
- 阶段三不为它们设计容器格式,`m_sScr` 继续按老格式写
- 降级导出对它们无事可做——本来就没进容器
- 老图打开时仍走 `ReCreate` 的 `StringToRegion` + `StringToPolyline` 重建,读路径保留
之前设想的「折线后缀与 4.3.6 停写 `m_sScr` 冲突」因此不成立:停写范围只覆盖迁移的 7 种类型。
### 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 验证清单
- 迁移的 7 种类型各建一个样本,导出后装到老版本插件里打开,几何、面积、位置都正确
- 老版本打开导出的图 → 另存 → 回新版打开,几何仍正确(回环)
- 导出前后,当前文档自身存盘仍是新格式(开关已复位)
- 容器被人为改坏(magic 对、region 段截断)时,`MakeLegacyScr` 返回 false,导出退回新格式并打日志,不产出空几何
- 遗留的 `kIntersect` / `kSliceSolid`(若老图里有)打开、另存、再打开,截面仍能重建
### 12.10 成本与时机
| 项 | 量 |
| --- | --- |
| 开关 + `dwgOutFields` 分支 | ~30 行 |
| `MakeLegacyScr` 按类型拼回 | ~100 行 |
| 导出命令 + 文件选择 | ~40 行 |
| 主要风险 | 老格式规则核对不全,导出的图在老版本里读不出来 |
12.6 的结论已经定:遗留类型不参与迁移,阶段三不必为它们设计容器格式。与阶段三的先后关系:两者最好一起上——阶段三一旦合入、`m_sScr` 停写,没有降级导出的那段时间里,新图在老版本插件上就是缺几何的。
---
## 13. 开图速度优化方案(2026-10-22)
### 13.1 现状与成本模型
- `AecDbX3dPartEntity::dwgInFields:1824` 在开图时无条件调用 `m_pPart->Create()`。全库扫描 `dwgInFields`,立即重建的只有这一处
- `AecDbX3dPart::Create()`(`:786`)先删掉 `m_pSolid`,再对每个 unite 成员调 `GetDraw3dSolid()`(`EnsureCreated` + `clone` + 旋转),逐个 `booleanOper(kBoolUnite)`,再跑 subtract 列表,最后整体旋转/平移、设颜色、把 `m_mapXData` 写成 `BIMDATA` XData
- 单个 part 的成本约 N×(acisIn + clone) + (N−1)×布尔。N 大、布尔对象复杂时是非线性增长,这正是「复杂几何由 boolean 得到」的场景
- `Create()` **没有「已是最新」判断**,每次调用都全量重建;`AddX3dSolid`(`:151`)只 append、不置脏,所以调用方只能靠再调一次 `Create()` 保证几何同步
### 13.2 方案 A:脏标记 + 懒创建(推荐,不改存储格式)
分两步,可以分开提交。
**A1 让 `Create()` 幂等**
| 位置 | 改动 |
| --- | --- |
| `inc/AecDbX3dPart.h:140` 附近 | 增加 `bool m_bDirty;`,构造(`:50`)置 `true` |
| `Create()` `:786` | 开头 `if (!m_bDirty && m_pSolid != NULL) return true;`;进入构建前先 `m_bDirty = false`,写法同 `AcDbX3dSolid::EnsureCreated`(`:4620`),避免重入时重复构建 |
| 改参数的接口 | 置 `m_bDirty = true`:`AddX3dSolid:151`、`RemoveAt:504`、`RemoveAll3dSolidRec:468`、`SetOrgPoint:170`、`SetAxis:582`、`SetBoolOperFor:766`、`SetHideShow:602`、`SetHide:608`、`SetActive:639`、`PeelOffTheSkin:623`、`AddMove:644`/`:655`、`ResetMove:671`、`AddRotate:682`、`ResetRotate:705`、`CopyFrom:1255` |
| 新增 | `bool EnsureCreated() const`:`m_bDirty` 时 `const_cast` 调 `Create()` |
现状里 `AddMove`、`AddRotate` 等已经 `DELETE_PTR(m_pSolid)` 做失效,改成置脏后语义一致,额外好处是能区分「已失效但还没人要」和「已重建」。
**A2 开图不建几何**
- `AecDbX3dPartEntity::dwgInFields:1824` 去掉 `m_pPart->Create()`,由 `AecDbX3dPart::dwgInFields` 结束时置脏
- 读入口统一先 `EnsureCreated()`:
| 入口 | 位置 |
| --- | --- |
| `CreateDraw3dSolid()` | `AecDbX3dPart.cpp:940` |
| `Draw3d()` | `:969` |
| `getOsnapPoints()` | `:1097` |
| `GetForHouseSubtract()` | `:903` |
| `AecDbX3dPartEntity::Draw3d()` | `AecDbX3dPartEntity.cpp:1755`,经 `CreateDraw3dSolid` 已覆盖,确认即可 |
`AecDbBDPipeBase.cpp:329` 那种「`CreateDraw3dSolid()` 返回空就 `Create()` 再试」的兜底可以删掉,`EnsureCreated` 已经包含在 `CreateDraw3dSolid` 里。
**收益边界**
- 关闭/冻结图层、视口外、XREF 附着、审核、发布等不绘制的场景:完全省掉
- 全图都在视口内:开图时间下降,首次 regen 补回来,总时间基本持平
- A1 单独也有收益:所有「改完参数就 `Create()`」的交互路径不再重复全量重建
**风险**
- `Create()` 有副作用:整体变换、`setColorIndex`、`m_mapXData.Write(m_pSolid, "BIMDATA")`(`:865-898`)。延迟后这些也延迟,凡是在建好之前读 solid 的代码都必须经过 `EnsureCreated`
- unite 列表为空时 `Create()` 返回 false 且 `m_pSolid == NULL`;`DTYG_TYPE` 且 `Hide` 的分支直接 `return true` 且不建体(`:792-799`)。`EnsureCreated` 的判定只看 `m_bDirty`,不能因为 `m_pSolid == NULL` 反复重试
- `Create()` 非 const,`EnsureCreated` 需要 `const_cast`
- `CopyFrom:1255` 里对每个 x3dsolid 调了 `ReCreate()`,可以一起删掉——`AcDbX3dSolid` 已有 `m_bNeedCreate`,懒加载会处理
### 13.3 方案 B:缓存布尔结果(改格式,暂不推荐)
把 part 最终的 solid 用 `acisOut` 存成 SAT,开图时一次 `acisIn` 代替 N 次建体 + N 次布尔。这是唯一真正减少计算量的办法,但两条落点都有代价:
| 落点 | 兼容性 | 问题 |
| --- | --- | --- |
| `AecDbX3dPart` 的 dwg 字段 | 需要升版本号,老插件整块变 proxy | 与阶段三取消的理由相同,不接受 |
| 实体扩展字典(xrecord 分块存 SAT) | 老插件忽略未知 xrecord,仍按参数重建 | 保存/打开后写缓存、失效判定、体积膨胀都要额外处理 |
体积上,布尔结果的 SAT 通常比输入参数之和大一个量级,复杂 part 可能到几百 KB,会打破「图纸大小可接受」这个前提。
结论:先做 A,量出开图耗时里布尔占多少,再决定 B 值不值。
### 13.4 方案 C:降低单次重建成本(可选)
- 合并前先按 bbox 剔除被完全包含的成员,少跑几次布尔
- `GetDraw3dSolid()` 每个成员 clone 一次完整 solid,N 个成员就是 N 次深拷贝;只参与一次布尔的成员可以改成「取原生指针直接参与运算」,但要逐个确认所有权(`booleanOper` 会消耗传入对象)
- `AecDbECFitting::DrawUserInf3d`(`AecDbECFitting.cpp:4001`)每次绘制都新建临时 `AecDbX3dPart` 并 `Create()`,和持久化对象无关,属于另一个热点
### 13.5 度量与验收
- 埋点:`AecDbX3dPart::Create()` 的调用次数与累计耗时、开图总时长、首次 regen 时长
- 场景:全图可见 / 一半图层冻结 / 缩放到局部 / 作为 XREF 附着
- 判据:开图时长下降;首次 regen 的增量不超过开图省下的量;移动、改参数等交互不退化
### 13.6 建议顺序
1. A1(`m_bDirty` + `Create()` 早退):改动小,先量交互路径的收益
2. A2(`dwgInFields` 去掉 `Create()`):开图收益
3. 量完再决定 B / C