# 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` 无法实例化:`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& regs, void*& pBuf, Adesk::UInt32& nLen); bool UnpackRegions(const void* pBuf, Adesk::UInt32 nLen, AcArray& 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